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
d873e19
add object motion control
nbbosco Apr 27, 2026
ea23ffc
readme + changelog
nbbosco Apr 27, 2026
7b6e771
chore: automated build of frontend assets
nbbosco Apr 27, 2026
5ba99c0
Merge branch 'main' of https://github.com/gramaziokohler/compas_three…
nbbosco Apr 29, 2026
0c4df65
Merge branch 'play/pause' of https://github.com/gramaziokohler/compas…
nbbosco Apr 29, 2026
8e6f63b
chore: automated build of frontend assets
nbbosco Apr 29, 2026
c774930
Merge branch 'main' of https://github.com/gramaziokohler/compas_three…
nbbosco May 5, 2026
934a064
chore: automated build of frontend assets
nbbosco May 5, 2026
4d94803
Merge branch 'main' of https://github.com/gramaziokohler/compas_three…
nbbosco May 7, 2026
d766c95
desactivate if no motion
nbbosco May 7, 2026
d31df5f
chore: automated build of frontend assets
nbbosco May 7, 2026
d2a6c87
Merge branch 'main' of https://github.com/gramaziokohler/compas_three…
nbbosco May 7, 2026
36c26c2
improve pause toggle
nbbosco May 7, 2026
19d89f0
Merge branch 'main' of https://github.com/gramaziokohler/compas_three…
nbbosco May 7, 2026
4e61321
chore: automated build of frontend assets
nbbosco May 7, 2026
606c17f
Merge branch 'main' of https://github.com/gramaziokohler/compas_three…
nbbosco May 7, 2026
ac85d00
refactor motion check
nbbosco May 7, 2026
d9428ad
Merge branch 'play/pause' of https://github.com/gramaziokohler/compas…
nbbosco May 7, 2026
af6ae39
chore: automated build of frontend assets
nbbosco May 7, 2026
da447b1
change icon logic
nbbosco May 8, 2026
270588f
chore: automated build of frontend assets
nbbosco May 8, 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
* Added, when an object is selected its material color changes to orange and slightly emissive yellow.
* Added `Toolbar.vue` featuring existing transforms and views shortcuts into user-friendly buttons.
* Added `NumberField` class to `compas_threejs.ui`.
* Added command to save views, select them and download a screenshot as image.
* Added commands to save views, select them and download a screenshot as image.
* Added dark/light theme toggle.
* Added command to play/pause object motion.

### Changed

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ View shortcuts (numpad):
- `0`: Bottom view

Display shortcuts:
- `spacebar`: Play/Pause object motion
- `S`: Save current view
- `F`: Save screenshot as image

Expand Down
16 changes: 13 additions & 3 deletions frontend/compas_threejs/src/communications/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { AnyData } from "../protobuff/generated/compas_pb/data/message";
import { Dictionary } from "../protobuff/messages";
import * as THREE from "three";
import { lightManager } from "../viewer/light_manager";
import { geometryManager } from "../viewer/geometry_manager";
import { geometryManager, SCENE_GEOMETRIES } from "../viewer/geometry_manager";
import { materialManager } from "../viewer/material_manager";
import { sceneManager } from "../viewer/scene_manager";
import { themeManager } from "../viewer/theme_manager";
Expand All @@ -13,8 +13,8 @@ import { textManager } from "../viewer/text_manager";
import { objectInfoManager } from "./objectInfo";
import { objectActionManager } from "./objectInfo";
import { removeObjectFromScene } from "../viewer/scene_manager";

const SCENE_GEOMETRIES: { [guid: string]: THREE.Object3D } = {};
import { objectMotionManager } from "./objectMotion";
import { motionState } from "@/store/store";

export function dispatchMessage(message: Uint8Array) {
const obj = unpackMessageToGeometry(message);
Expand All @@ -23,6 +23,13 @@ export function dispatchMessage(message: Uint8Array) {
analyzeDictionary(obj);
return;
} else {
if (
motionState.objectMotionPaused &&
obj?.guid &&
SCENE_GEOMETRIES[obj.guid]
) {
return;
}
geometryManager(obj);
}
}
Expand Down Expand Up @@ -59,6 +66,9 @@ function analyzeDictionary(dictionary: Dictionary) {
case "remove_object":
removeObjectFromScene(data);
break;
case "object_motion":
objectMotionManager(data);
break;
default:
console.warn("Unknown dispatch value:", data.dispatch.value);
}
Expand Down
57 changes: 57 additions & 0 deletions frontend/compas_threejs/src/communications/objectMotion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { sendData } from "@/communications/communication";
import { motionState } from "@/store/store";

function notifyBackendObjectMotionPause(paused: boolean): void {
sendData({
dispatch: "object_motion_control",
paused,
});
}

export function setObjectMotionPaused(paused: boolean): void {
if (!motionState.objectMotionAvailable) {
return;
}

if (motionState.objectMotionPaused === paused) {
return;
}

motionState.objectMotionPaused = paused;
notifyBackendObjectMotionPause(paused);
}

export function toggleObjectMotionPaused(): boolean {
if (!motionState.objectMotionAvailable) {
return motionState.objectMotionPaused;
}

setObjectMotionPaused(!motionState.objectMotionPaused);
return motionState.objectMotionPaused;
}

export function isObjectMotionPaused(): boolean {
return motionState.objectMotionPaused;
}

export function setObjectMotionAvailable(available: boolean): void {
motionState.objectMotionAvailable = available;

if (!available) {
motionState.objectMotionPaused = false;
}
}

export function isObjectMotionAvailable(): boolean {
return motionState.objectMotionAvailable;
}

export function objectMotionManager(data: { [key: string]: any }): void {
switch (data.type.value) {
case "availability":
setObjectMotionAvailable(Boolean(data.enabled.value));
break;
default:
console.warn("Unknown object motion type:", data.type.value);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<template>
<div class="display-tools-wrapper">
<div class="toolbar-group">
<ToggleMovementButton
Comment thread
nbbosco marked this conversation as resolved.
v-if="motionAvailable"
:active="motionPaused"
@toggled="setMotionPaused"
/>
<SaveViewButton
:default-name="`View ${savedViews.length + 1}`"
@saved="handleSavedView"
Expand All @@ -17,26 +22,34 @@
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import { computed, onMounted, ref } from "vue";
import {
toggleObjectMotionPaused,
applySavedView,
captureCurrentView,
saveCurrentCanvasImage,
toggleTheme,
type SavedView,
} from "@/viewer/toolbar_actions";
import { useKeyboardShortcuts } from "@/components/tools/useKeyboardShortcuts";
import { motionState } from "@/store/store";
import {
ToggleMovementButton,
SaveViewButton,
SavedViewsButton,
SaveScreenshotButton,
} from "./index";

const SAVED_VIEWS_STORAGE_KEY = "compas_threejs_saved_views";

const motionPaused = computed(() => motionState.objectMotionPaused);
const motionAvailable = computed(() => motionState.objectMotionAvailable);
const savedViews = ref<SavedView[]>([]);
const selectedSavedViewId = ref<string>("");

function setMotionPaused(paused: boolean) {
motionState.objectMotionPaused = paused;
}

function persistSavedViews() {
localStorage.setItem(SAVED_VIEWS_STORAGE_KEY, JSON.stringify(savedViews.value));
}
Expand Down Expand Up @@ -111,6 +124,13 @@ useKeyboardShortcuts({
d: () => {
toggleTheme();
},
" ": () => {
if (!motionAvailable.value) {
return;
}

setMotionPaused(toggleObjectMotionPaused());
},
});

</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<template>
<TooltipProvider :delay-duration="600">
<Tooltip>
<TooltipTrigger>
<Button
variant="secondary"
size="icon"
:class="{ active }"
@click="handleClick"
>
<Pause class="button-icon" :size="16" />
</Button>
</TooltipTrigger>
<TooltipContent class="z-1000" side="bottom">
<p>Play/Pause motion <Kbd>spacebar</Kbd></p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</template>

<script setup lang="ts">
import { Pause } from "lucide-vue-next";
import { toggleObjectMotionPaused } from "@/viewer/toolbar_actions";
import { Button } from "@/components/ui/button";
import { Kbd } from "@/components/ui/kbd";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from "@/components/ui/tooltip";

const props = defineProps<{
active: boolean;
}>();

const emit = defineEmits<{
(e: "toggled", paused: boolean): void;
}>();

function handleClick() {
const paused = toggleObjectMotionPaused();
emit("toggled", paused);
}
</script>
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as ToggleMovementButton } from "./ToggleMovementButton.vue";
export { default as SaveViewButton } from "./SaveViewButton.vue";
export { default as SavedViewsButton } from "./SavedViewsButton.vue";
export { default as SaveScreenshotButton } from "./SaveScreenshotButton.vue";
7 changes: 5 additions & 2 deletions frontend/compas_threejs/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const pickerMode = reactive({ value: "translate" });
export const blockPicker = reactive({ value: false });
export const showEdges = reactive({ value: false });

export const theme = reactive({
value: "light",
export const theme = reactive({ value: "light" });

export const motionState = reactive({
objectMotionPaused: false,
objectMotionAvailable: false,
});
2 changes: 2 additions & 0 deletions frontend/compas_threejs/src/viewer/toolbar_actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { setCameraViewPreset } from "./scene_manager";
import { setTransformMode } from "./picker";
import { toggleTheme } from "./theme_manager";
import { toggleObjectMotionPaused } from "@/communications/objectMotion";
import {
saveCurrentCanvasImage,
applySavedView,
Expand All @@ -14,6 +15,7 @@ export {
setCameraViewPreset,

// Group 3: view management and export
toggleObjectMotionPaused,
captureCurrentView,
applySavedView,
saveCurrentCanvasImage,
Expand Down
2 changes: 1 addition & 1 deletion src/compas_threejs/viewer/frontend/assets/index.css

Large diffs are not rendered by default.

352 changes: 176 additions & 176 deletions src/compas_threejs/viewer/frontend/assets/index.js

Large diffs are not rendered by default.

23 changes: 21 additions & 2 deletions src/compas_threejs/viewer/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def __init__(
self._geometry_registry = dict()
self._metadata_registry = dict()
self._object_actions_registry = dict()
self._object_motion_paused = False

def __enter__(self):
return self
Expand All @@ -120,6 +121,7 @@ def loop(self) -> callable:
@loop.setter
def loop(self, callback: callable):
self._loop = callback
self._send_object_motion_state()

@property
def loop_interval(self) -> float:
Expand Down Expand Up @@ -361,6 +363,7 @@ def start(self, show=False):
# Ensure frontend receives current dark mode state on start
self.dark_mode = self._dark_mode
self.camera_damping = self.camera_damping
self._send_object_motion_state()
self._send_default_view()

# Send default lighting
Expand All @@ -383,9 +386,9 @@ def start(self, show=False):
while True:
time.sleep(self.loop_interval)
callback = self.loop
if callback:
if callback and not self._object_motion_paused:
callback(i)
i += 1
i += 1
except KeyboardInterrupt:
console.log("[green]Interruption ordered[/green]")
finally:
Expand Down Expand Up @@ -413,6 +416,14 @@ def _send_dictionary_message(self, msg: dict):
# Queue the message if the server is not running yet
self.queued_messages.append((binary_data, ""))

def _send_object_motion_state(self):
message = {
"dispatch": "object_motion",
"type": "availability",
"enabled": bool(self._loop),
}
self._send_dictionary_message(message)

# ---- GEOMETRY --------------------------------------------------------------------------------

def add_geometry(
Expand Down Expand Up @@ -639,6 +650,8 @@ def on_message(self, message):
self.manage_picked_object(action_dictionary)
elif action_dictionary.get("dispatch") == "object_action_callback":
self.manage_object_action_callback(action_dictionary)
elif action_dictionary.get("dispatch") == "object_motion_control":
self.manage_object_motion_control(action_dictionary)
else:
console.log(
f"[yellow]Received unrecognized message from frontend: {action_dictionary}[/yellow]"
Expand Down Expand Up @@ -719,3 +732,9 @@ def manage_object_action_callback(self, action_dictionary):
self._buttons[action_id](object, value)
else:
print(f"Unrecognized action or missing handler for action ID: {action_id}")

def manage_object_motion_control(self, action_dictionary):
paused = bool(action_dictionary.get("paused", False))
self._object_motion_paused = paused
state = "paused" if paused else "running"
console.log(f"[blue]Object motion loop state set to: {state}[/blue]")