Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
74 changes: 74 additions & 0 deletions src/controls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export class URDFControls extends GUI {
private _sceneFolder: any;
private _jointsFolder: any;
private _jointsEditorFolder: any;
private _linksFolder: any;
private _workingPath = '';

controls: any = {
Expand All @@ -28,6 +29,7 @@ export class URDFControls extends GUI {
},
joints: {},
lights: {},
links: {},
editor: {}
};

Expand All @@ -53,6 +55,9 @@ export class URDFControls extends GUI {
this._jointsFolder = this.addFolder('Joints');
this._jointsFolder.domElement.setAttribute('class', 'dg joints-folder');

this._linksFolder = this.addFolder('Links');
this._linksFolder.domElement.setAttribute('class', 'dg links-folder');

this._jointsEditorFolder = this.addFolder('Joints Editor');
this._jointsEditorFolder.domElement.setAttribute(
'class',
Expand Down Expand Up @@ -97,6 +102,13 @@ export class URDFControls extends GUI {
return this._jointsEditorFolder;
}

/**
* Retrieves the folder with links settings
*/
get linksFolder() {
return this._linksFolder;
}

/**
* Checks if a given object is empty {}
*
Expand Down Expand Up @@ -556,4 +568,66 @@ export class URDFControls extends GUI {

return this.controls.editor;
}

/**
* Creates controls for link visualization (frames, opacity)
*
* @param linkNames - Array of available link names
* @returns - The controls to trigger callbacks when link settings change
*/
createLinkControls(linkNames: string[] = []) {
if (this._isEmpty(this.controls.links)) {
const globalLinkSettings = {
'Axis Indicator': false,
'Frame Size': 1
};

this.controls.links.axisIndicator = this._linksFolder.add(
globalLinkSettings,
'Axis Indicator'
);

this.controls.links.frameSize = this._linksFolder.add(
globalLinkSettings,
'Frame Size',
0.1,
10,
0.05
);

this._enforceNumericInput(this.controls.links.frameSize);

// Individual link controls subfolder
const individualLinksFolder =
this._linksFolder.addFolder('Individual Links');
this.controls.links.individual = {};

// Create controls for each link
linkNames.forEach(linkName => {
const linkFolder = individualLinksFolder.addFolder(linkName);

const linkSettings = {
'Show Frame': false,
Opacity: 1.0
};

this.controls.links.individual[linkName] = {
showFrame: linkFolder.add(linkSettings, 'Show Frame'),
opacity: linkFolder.add(linkSettings, 'Opacity', 0.0, 1.0, 0.01)
};

// Enforce numeric input for opacity slider
this._enforceNumericInput(
this.controls.links.individual[linkName].opacity
);

linkFolder.close();
});

individualLinksFolder.close();
this._linksFolder.open();
}

return this.controls.links;
}
}
77 changes: 63 additions & 14 deletions src/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ export class URDFLayout extends PanelLayout {
this._setSceneControls();
this._setJointControls();
this._setLightControls();
this._setLinkControls();
this._setEditorControls();
}

Expand Down Expand Up @@ -235,6 +236,53 @@ export class URDFLayout extends PanelLayout {
});
}

/**
* Set callbacks for the link controls in the panel.
*/
private _setLinkControls(): void {
// Add link controls with link names
const linkNames = Object.keys(this._loader.robotModel.links);
const linkControls = this._controlsPanel.createLinkControls(linkNames);

// Axis indicator control
linkControls.axisIndicator.onChange((show: boolean) => {
this._renderer.setAxisIndicatorVisibility(show);
});

linkControls.frameSize.onChange((size: number) => {
// Update individual frames with new size
if (linkControls.individual) {
Object.keys(linkControls.individual).forEach(linkName => {
const showIndividual =
linkControls.individual[linkName].showFrame.getValue();
if (showIndividual) {
this._renderer.setIndividualFrameVisibility(linkName, true, size);
}
});
}
});

// Individual link controls
if (linkControls.individual) {
Object.keys(linkControls.individual).forEach(linkName => {
// Frame visibility
linkControls.individual[linkName].showFrame.onChange(
(show: boolean) => {
const size = linkControls.frameSize.getValue();
this._renderer.setIndividualFrameVisibility(linkName, show, size);
}
);

// Link opacity control
linkControls.individual[linkName].opacity.onChange(
(opacity: number) => {
this._renderer.setLinkOpacity(linkName, opacity);
}
);
});
}
}

/**
* Set callback for changing directional light position in the controls panel.
*/
Expand Down Expand Up @@ -596,11 +644,11 @@ export class URDFLayout extends PanelLayout {
this._interactionEditor.unHighlightLink('parent');
this._selectedLinks.parent = { name: null, obj: null };
} else {
const link = this._loader.robotModel.links[linkName];
const linkObject = link.children.find((c: any) => c.isURDFVisual)
?.children[0];
const linkObject = this._renderer.getLinkObject(linkName);
this._selectedLinks.parent = { name: linkName, obj: linkObject };
this._interactionEditor.highlightLink(linkObject, 'parent');
if (linkObject) {
this._interactionEditor.highlightLink(linkObject, 'parent');
}
}
updateJointName();
});
Expand All @@ -615,11 +663,11 @@ export class URDFLayout extends PanelLayout {
this._interactionEditor.unHighlightLink('child');
this._selectedLinks.child = { name: null, obj: null };
} else {
const link = this._loader.robotModel.links[linkName];
const linkObject = link.children.find((c: any) => c.isURDFVisual)
?.children[0];
const linkObject = this._renderer.getLinkObject(linkName);
this._selectedLinks.child = { name: linkName, obj: linkObject };
this._interactionEditor.highlightLink(linkObject, 'child');
if (linkObject) {
this._interactionEditor.highlightLink(linkObject, 'child');
}
}
updateJointName();
});
Expand Down Expand Up @@ -655,24 +703,25 @@ export class URDFLayout extends PanelLayout {
parentLink: string,
childLink: string
): void {
this._interactionEditor.clearHighlights();
if (parentLink !== 'none') {
const link = this._loader.robotModel.links[parentLink];
const linkObject = link?.children.find((c: any) => c.isURDFVisual)
?.children[0];
const linkObject = this._renderer.getLinkObject(parentLink);
this._selectedLinks.parent = { name: parentLink, obj: linkObject };
if (linkObject) {
this._interactionEditor.highlightLink(linkObject, 'parent');
}
} else {
this._selectedLinks.parent = { name: null, obj: null };
}

if (childLink !== 'none') {
const link = this._loader.robotModel.links[childLink];
const linkObject = link?.children.find((c: any) => c.isURDFVisual)
?.children[0];
const linkObject = this._renderer.getLinkObject(childLink);
this._selectedLinks.child = { name: childLink, obj: linkObject };
if (linkObject) {
this._interactionEditor.highlightLink(linkObject, 'child');
}
} else {
this._selectedLinks.child = { name: null, obj: null };
}
}

Expand Down
49 changes: 49 additions & 0 deletions src/links/axisIndicator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as THREE from 'three';
import { ViewHelper } from 'three/examples/jsm/helpers/ViewHelper.js';

/**
* A helper class to manage and render an axis indicator using THREE.ViewHelper.
*/
export class AxisIndicatorHelper {
private _viewHelper: ViewHelper;
private _proxyCamera: THREE.Camera;
public visible = false;

/**
* @param camera The main scene camera to derive orientation from.
* @param domElement The renderer's DOM element for ViewHelper instantiation.
*/
constructor(camera: THREE.Camera, domElement: HTMLElement) {
// Create a dedicated camera for the ViewHelper.
// It will be synchronized with the main camera before rendering.
this._proxyCamera = camera.clone();
this._viewHelper = new ViewHelper(this._proxyCamera, domElement);
}

/**
* Renders the axis indicator.
* This should be called in the main render loop, after the main scene has been rendered.
* @param renderer The main WebGLRenderer instance.
* @param mainCamera The main scene camera to sync with.
*/
public render(renderer: THREE.WebGLRenderer, mainCamera: THREE.Camera): void {
if (!this.visible) {
return;
}

this._proxyCamera.quaternion.copy(mainCamera.quaternion);
const rotationX = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(1, 0, 0),
Math.PI / 2
);
this._proxyCamera.quaternion.premultiply(rotationX);
this._viewHelper.render(renderer);
}

/**
* Disposes of the internal ViewHelper.
*/
public dispose(): void {
this._viewHelper.dispose();
}
}
Loading
Loading