diff --git a/examples/franka_panda_description/meshes/visual/link0.dae b/examples/franka_panda_description/meshes/visual/link0.dae
index dc4c718..021660b 100644
--- a/examples/franka_panda_description/meshes/visual/link0.dae
+++ b/examples/franka_panda_description/meshes/visual/link0.dae
@@ -50,92 +50,6 @@
-
-
-
-
- 1000 1000 1000
- 1
- 0
- 0.00111109
-
-
-
-
- 0
- 0
- 1
- 1
- 1
- 1
- 1
- 0
- 0
- 0
- 1000
- 29.99998
- 75
- 0.15
- 0
- 1
- 2
- 0.04999995
- 30.002
- 1
- 3
- 2880
- 3
- 1
- 1
- 0.1
- 0.1
- 1
-
-
-
-
-
-
- 1000 1000 1000
- 1
- 0
- 0.00111109
-
-
-
-
- 0
- 0
- 1
- 1
- 1
- 1
- 1
- 0
- 0
- 0
- 1000
- 29.99998
- 75
- 0.15
- 0
- 1
- 2
- 0.04999995
- 30.002
- 1
- 3
- 2880
- 3
- 1
- 1
- 0.1
- 0.1
- 1
-
-
-
-
diff --git a/src/controls.ts b/src/controls.ts
index 690e28d..8438539 100644
--- a/src/controls.ts
+++ b/src/controls.ts
@@ -25,7 +25,8 @@ export class URDFControls extends GUI {
grid: {},
height: {}
},
- joints: {}
+ joints: {},
+ lights: {}
};
/**
@@ -47,6 +48,9 @@ export class URDFControls extends GUI {
});
// Create folders
+ this._jointsFolder = this.addFolder('Joints');
+ this._jointsFolder.domElement.setAttribute('class', 'dg joints-folder');
+
this._workspaceFolder = this.addFolder('Workspace');
this._workspaceFolder.domElement.setAttribute(
'class',
@@ -55,9 +59,6 @@ export class URDFControls extends GUI {
this._sceneFolder = this.addFolder('Scene');
this._sceneFolder.domElement.setAttribute('class', 'dg scene-folder');
-
- this._jointsFolder = this.addFolder('Joints');
- this._jointsFolder.domElement.setAttribute('class', 'dg joints-folder');
}
/**
@@ -262,5 +263,133 @@ export class URDFControls extends GUI {
document.addEventListener('mouseup', onMouseUp);
}
});
+
+ // Show resize cursor when hovering near left edge
+ this.domElement.addEventListener('mousemove', (e: MouseEvent) => {
+ const rect = this.domElement.getBoundingClientRect();
+ const offsetX = e.clientX - rect.left;
+ this.domElement.style.cursor =
+ offsetX < grabZoneWidth || isResizing ? 'ew-resize' : 'auto';
+ });
+ this.domElement.addEventListener('mouseleave', () => {
+ if (!isResizing) {
+ this.domElement.style.cursor = 'auto';
+ }
+ });
+ }
+
+ /**
+ * Creates controls for the different lights in the scene
+ *
+ * @returns - The controls to trigger callbacks when light settings change
+ */
+ createLightControls() {
+ if (this._isEmpty(this.controls.lights)) {
+ // Create subfolders for each light
+ const directionalFolder =
+ this._sceneFolder.addFolder('Directional Light');
+ const ambientFolder = this._sceneFolder.addFolder('Ambient Light');
+ const hemisphereFolder = this._sceneFolder.addFolder('Hemisphere Light');
+
+ // Initialize settings for each light type
+ const directionalSettings = {
+ Altitude: Math.PI / 4, // 45 degrees elevation
+ Azimuth: Math.PI / 4, // 45 degrees around vertical axis
+ Color: [255, 255, 255],
+ Intensity: 1.0,
+ ShowHelper: false
+ };
+
+ const ambientSettings = {
+ Color: [255, 255, 255],
+ Intensity: 0.5
+ };
+
+ const hemisphereSettings = {
+ SkyColor: [255, 255, 255],
+ GroundColor: [38, 50, 56],
+ Intensity: 1.0,
+ ShowHelper: false
+ };
+
+ // Spherical coordinate angle limits and steps
+ const minAngle = -Math.PI;
+ const maxAngle = Math.PI;
+ const angleStep = 0.01;
+
+ // Intensity limits and steps
+ const minIntensity = 0;
+ const maxIntensity = 10;
+ const intensityStep = 0.1;
+
+ // Controls for directional light
+ this.controls.lights.directional = {
+ position: {
+ altitude: directionalFolder.add(
+ directionalSettings,
+ 'Altitude',
+ minAngle,
+ maxAngle,
+ angleStep
+ ),
+ azimuth: directionalFolder.add(
+ directionalSettings,
+ 'Azimuth',
+ minAngle,
+ maxAngle,
+ angleStep
+ )
+ },
+ color: directionalFolder.addColor(directionalSettings, 'Color'),
+ intensity: directionalFolder.add(
+ directionalSettings,
+ 'Intensity',
+ minIntensity,
+ maxIntensity,
+ intensityStep
+ ),
+ showHelper: directionalFolder
+ .add(directionalSettings, 'ShowHelper')
+ .name('Show Helper')
+ };
+
+ // Ambient light controls
+ this.controls.lights.ambient = {
+ color: ambientFolder.addColor(ambientSettings, 'Color'),
+ intensity: ambientFolder.add(
+ ambientSettings,
+ 'Intensity',
+ minIntensity,
+ maxIntensity,
+ intensityStep
+ )
+ };
+
+ // Hemisphere light controls
+ this.controls.lights.hemisphere = {
+ skyColor: hemisphereFolder
+ .addColor(hemisphereSettings, 'SkyColor')
+ .name('Sky Color'),
+ groundColor: hemisphereFolder
+ .addColor(hemisphereSettings, 'GroundColor')
+ .name('Ground Color'),
+ intensity: hemisphereFolder.add(
+ hemisphereSettings,
+ 'Intensity',
+ minIntensity,
+ maxIntensity,
+ intensityStep
+ ),
+ showHelper: hemisphereFolder
+ .add(hemisphereSettings, 'ShowHelper')
+ .name('Show Helper')
+ };
+
+ // Open Scene (lights) and directional subfolder
+ this._sceneFolder.open();
+ directionalFolder.open();
+ }
+
+ return this.controls.lights;
}
}
diff --git a/src/layout.ts b/src/layout.ts
index 790745c..c09b42d 100644
--- a/src/layout.ts
+++ b/src/layout.ts
@@ -165,6 +165,7 @@ export class URDFLayout extends PanelLayout {
this._setPathControls();
this._setSceneControls();
this._setJointControls();
+ this._setLightControls();
}
/**
@@ -214,6 +215,72 @@ export class URDFLayout extends PanelLayout {
});
}
+ /**
+ * Set callback for changing directional light position in the controls panel.
+ */
+ private _setLightControls(): void {
+ const lightControl = this._controlsPanel.createLightControls();
+
+ // Directional light callbacks
+ const directional = lightControl.directional;
+
+ // Position controls using spherical coordinates
+ directional.position.altitude.onChange((newAltitude: number) => {
+ const azimuth = directional.position.azimuth.getValue();
+ this._renderer.setDirectionalLightPositionSpherical(newAltitude, azimuth);
+ });
+
+ directional.position.azimuth.onChange((newAzimuth: number) => {
+ const altitude = directional.position.altitude.getValue();
+ this._renderer.setDirectionalLightPositionSpherical(altitude, newAzimuth);
+ });
+
+ // Color and intensity controls
+ directional.color.onChange((newColor: number[]) => {
+ this._renderer.setDirectionalLightColor(newColor);
+ });
+
+ directional.intensity.onChange((newIntensity: number) => {
+ this._renderer.setDirectionalLightIntensity(newIntensity);
+ });
+
+ // Helper visibility toggle for directional light
+ directional.showHelper.onChange((visible: boolean) => {
+ this._renderer.setDirectionalLightHelperVisibility(visible);
+ });
+
+ // Ambient light callbacks
+ const ambient = lightControl.ambient;
+
+ ambient.color.onChange((newColor: number[]) => {
+ this._renderer.setAmbientLightColor(newColor);
+ });
+
+ ambient.intensity.onChange((newIntensity: number) => {
+ this._renderer.setAmbientLightIntensity(newIntensity);
+ });
+
+ // Hemisphere light callbacks
+ const hemisphere = lightControl.hemisphere;
+
+ hemisphere.skyColor.onChange((newColor: number[]) => {
+ this._renderer.setHemisphereLightSkyColor(newColor);
+ });
+
+ hemisphere.groundColor.onChange((newColor: number[]) => {
+ this._renderer.setHemisphereLightGroundColor(newColor);
+ });
+
+ hemisphere.intensity.onChange((newIntensity: number) => {
+ this._renderer.setHemisphereLightIntensity(newIntensity);
+ });
+
+ // Helper visibility toggle for hemisphere light
+ hemisphere.showHelper.onChange((visible: boolean) => {
+ this._renderer.setHemisphereLightHelperVisibility(visible);
+ });
+ }
+
/**
* Set value for robot joint
*
diff --git a/src/renderer.ts b/src/renderer.ts
index fb09138..93a7320 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -25,6 +25,8 @@ export class URDFRenderer extends THREE.WebGLRenderer {
private _colorGround = new THREE.Color();
private _gridHeight = 0;
private _robotIndex = -1;
+ private _directionalLightHelper: THREE.DirectionalLightHelper | null = null;
+ private _hemisphereLightHelper: THREE.HemisphereLightHelper | null = null;
/**
* Creates a renderer to manage the scene elements
@@ -55,8 +57,17 @@ export class URDFRenderer extends THREE.WebGLRenderer {
this._controls = new OrbitControls(this._camera, this.domElement);
this._initControls();
- }
+ this.printSceneLights();
+ }
+ printSceneLights() {
+ console.log('Scene Lights:');
+ this._scene.children.forEach(obj => {
+ if (obj.type.includes('Light')) {
+ console.log(obj);
+ }
+ });
+ }
/**
* Initializes the camera
*/
@@ -123,28 +134,49 @@ export class URDFRenderer extends THREE.WebGLRenderer {
* Adds three lights to the scene
*/
private _addLights(): void {
- const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
+ // Directional light
+ const directionalLight = new THREE.DirectionalLight(0xfff2cc, 1.8);
directionalLight.castShadow = true;
- directionalLight.position.set(3, 10, 3);
- directionalLight.shadow.camera.top = 2;
- directionalLight.shadow.camera.bottom = -2;
- directionalLight.shadow.camera.left = -2;
- directionalLight.shadow.camera.right = 2;
- directionalLight.shadow.camera.near = 0.1;
- directionalLight.shadow.camera.far = 40;
+ directionalLight.position.set(3, 3, 3);
+ directionalLight.shadow.camera.top = 5;
+ directionalLight.shadow.camera.bottom = -5;
+ directionalLight.shadow.camera.left = -5;
+ directionalLight.shadow.camera.right = 5;
+ directionalLight.shadow.camera.near = 0.5;
+ directionalLight.shadow.camera.far = 50;
this._scene.add(directionalLight);
- const ambientLight = new THREE.AmbientLight('#fff');
- ambientLight.intensity = 0.5;
+ // Directional light helper
+ this._directionalLightHelper = new THREE.DirectionalLightHelper(
+ directionalLight,
+ 2,
+ new THREE.Color(0x000000)
+ );
+ this._directionalLightHelper.visible = false;
+ this._scene.add(this._directionalLightHelper);
+
+ // Ambient light
+ const ambientLight = new THREE.AmbientLight(0x404040);
+ ambientLight.intensity = 0.1;
ambientLight.position.set(0, 5, 0);
this._scene.add(ambientLight);
+ // Hemisphere light
const hemisphereLight = new THREE.HemisphereLight(
- this._colorSky,
- this._colorGround
+ 0x8888ff, // cool sky
+ 0x442200, // warm ground
+ 0.4
);
- hemisphereLight.intensity = 1;
this._scene.add(hemisphereLight);
+
+ // Hemisphere light helper
+ this._hemisphereLightHelper = new THREE.HemisphereLightHelper(
+ hemisphereLight,
+ 2
+ );
+ this._hemisphereLightHelper.material.color.set(0x000000);
+ this._hemisphereLightHelper.visible = false; // Set to hidden by default
+ this._scene.add(this._hemisphereLightHelper);
}
/**
@@ -162,6 +194,58 @@ export class URDFRenderer extends THREE.WebGLRenderer {
this._scene.children[hemisphereIndex] = hemisphereLight;
}
+ /**
+ * Toggle the visibility of the directional light helper
+ *
+ * @param visible - Whether the helper should be visible
+ */
+ setDirectionalLightHelperVisibility(visible: boolean): void {
+ if (this._directionalLightHelper) {
+ this._directionalLightHelper.visible = visible;
+ this.redraw();
+ }
+ }
+
+ /**
+ * Toggle the visibility of the hemisphere light helper
+ *
+ * @param visible - Whether the helper should be visible
+ */
+ setHemisphereLightHelperVisibility(visible: boolean): void {
+ if (this._hemisphereLightHelper) {
+ this._hemisphereLightHelper.visible = visible;
+ this.redraw();
+ }
+ }
+
+ /**
+ * Updates the position of the directional light using spherical coordinates
+ *
+ * @param altitude - Angle in radians from the horizontal plane (elevation)
+ * @param azimuth - Angle in radians around the vertical axis
+ */
+ setDirectionalLightPositionSpherical(
+ altitude: number,
+ azimuth: number
+ ): void {
+ const directionalLight = this._scene.children.find(
+ obj => obj.type === 'DirectionalLight'
+ ) as THREE.DirectionalLight;
+
+ if (directionalLight) {
+ const distance = 3;
+ const x = distance * Math.cos(altitude) * Math.cos(azimuth);
+ const z = distance * Math.cos(altitude) * Math.sin(azimuth);
+ const y = distance * Math.sin(altitude);
+
+ directionalLight.position.set(x, y, z);
+ if (this._directionalLightHelper) {
+ this._directionalLightHelper.update();
+ }
+ this.redraw();
+ }
+ }
+
/**
* Change the background color of the scene
*
@@ -226,6 +310,141 @@ export class URDFRenderer extends THREE.WebGLRenderer {
this.redraw();
}
+ /**
+ * Updates the position of the directional light
+ *
+ * @param x - The new x position
+ * @param y - The new y position
+ * @param z - The new z position
+ */
+ setDirectionalLightPosition(x: number, y: number, z: number): void {
+ const directionalLight = this._scene.children.find(
+ obj => obj.type === 'DirectionalLight'
+ ) as THREE.DirectionalLight;
+
+ if (directionalLight) {
+ directionalLight.position.set(x, y, z);
+ if (this._directionalLightHelper) {
+ this._directionalLightHelper.update();
+ }
+ this.redraw();
+ }
+ }
+
+ /**
+ * Updates the color of the directional light
+ *
+ * @param newColor - The new color as [R, G, B] array 0-255
+ */
+ setDirectionalLightColor(newColor: number[]): void {
+ const directionalLight = this._scene.children.find(
+ obj => obj.type === 'DirectionalLight'
+ ) as THREE.DirectionalLight;
+
+ if (directionalLight) {
+ directionalLight.color = new THREE.Color(...newColor.map(x => x / 255));
+ this.redraw();
+ }
+ }
+
+ /**
+ * Updates the intensity of the directional light
+ *
+ * @param intensity - The new intensity value
+ */
+ setDirectionalLightIntensity(intensity: number): void {
+ const directionalLight = this._scene.children.find(
+ obj => obj.type === 'DirectionalLight'
+ ) as THREE.DirectionalLight;
+
+ if (directionalLight) {
+ directionalLight.intensity = intensity;
+ this.redraw();
+ }
+ }
+
+ /**
+ * Updates the color of the ambient light
+ *
+ * @param newColor - The new color as [R, G, B] array 0-255
+ */
+ setAmbientLightColor(newColor: number[]): void {
+ const ambientLight = this._scene.children.find(
+ obj => obj.type === 'AmbientLight'
+ ) as THREE.AmbientLight;
+
+ if (ambientLight) {
+ ambientLight.color = new THREE.Color(...newColor.map(x => x / 255));
+ this.redraw();
+ }
+ }
+
+ /**
+ * Updates the intensity of the ambient light
+ *
+ * @param intensity - The new intensity value
+ */
+ setAmbientLightIntensity(intensity: number): void {
+ const ambientLight = this._scene.children.find(
+ obj => obj.type === 'AmbientLight'
+ ) as THREE.AmbientLight;
+
+ if (ambientLight) {
+ ambientLight.intensity = intensity;
+ this.redraw();
+ }
+ }
+
+ /**
+ * Updates the hemisphere light sky color
+ *
+ * @param newColor - The new color as [R, G, B] array 0-255
+ */
+ setHemisphereLightSkyColor(newColor: number[]): void {
+ const hemisphereLight = this._scene.children.find(
+ obj => obj.type === 'HemisphereLight'
+ ) as THREE.HemisphereLight;
+
+ if (hemisphereLight) {
+ hemisphereLight.color = new THREE.Color(...newColor.map(x => x / 255));
+ this.redraw();
+ }
+ }
+
+ /**
+ * Updates the hemisphere light ground color
+ *
+ * @param newColor - The new color as [R, G, B] array 0-255
+ */
+ setHemisphereLightGroundColor(newColor: number[]): void {
+ const hemisphereLight = this._scene.children.find(
+ obj => obj.type === 'HemisphereLight'
+ ) as THREE.HemisphereLight;
+
+ if (hemisphereLight) {
+ hemisphereLight.groundColor = new THREE.Color(
+ ...newColor.map(x => x / 255)
+ );
+ this.redraw();
+ }
+ }
+
+ /**
+ * Updates the hemisphere light intensity
+ *
+ * @param intensity - The new intensity value
+ */
+ setHemisphereLightIntensity(intensity: number): void {
+ const hemisphereLight = this._scene.children.find(
+ obj => obj.type === 'HemisphereLight'
+ ) as THREE.HemisphereLight;
+
+ if (hemisphereLight) {
+ hemisphereLight.intensity = intensity;
+ this.redraw();
+ }
+ }
+
/**
* Refreshes the viewer by re-rendering the scene and its elements
*/
diff --git a/style/base.css b/style/base.css
index 6d84b22..d78c8fc 100644
--- a/style/base.css
+++ b/style/base.css
@@ -10,7 +10,7 @@
--gui-color-input-bg: color-mix(
in hsl,
var(--jp-layout-color1),
- var(--jp-layout-color2)
+ var (--jp-layout-color2)
);
--gui-color-font: var(--jp-ui-font-color1);
--gui-color-accent: var(--jp-brand-color0);
@@ -29,16 +29,9 @@
box-sizing: border-box;
min-width: 150px;
max-width: 98%;
- padding-bottom: 20px;
-}
-
-/* Add a resize handle on the left border */
-.urdf-gui::before {
- content: '';
- position: absolute;
- width: 12px;
- height: 100%;
- cursor: ew-resize;
+ max-height: 75%;
+ overflow-y: auto;
+ overscroll-behavior-y: none;
}
.urdf-gui li.folder {
@@ -62,11 +55,23 @@
justify-content: center;
}
+.urdf-gui li.cr.string .property-name {
+ flex: 1 1;
+}
+
+.urdf-gui li.cr.color .property-name {
+ flex: 1 1;
+}
+
.urdf-gui .cr.function .property-name:hover {
background: var(--gui-color-accent);
border: 1px solid var(--gui-color-accent);
}
+.urdf-gui li.cr.number.has-slider .property-name {
+ flex: 1 1;
+}
+
.urdf-gui .cr.color,
.urdf-gui .cr.number,
.urdf-gui .cr.function,
@@ -141,35 +146,38 @@
overflow: scroll;
}
-/* Style for Dat.GUI's close button */
+/* Style for Dat.GUI's close button (sticky at bottom) */
.urdf-gui .close-button {
width: 100% !important;
color: white !important;
- position: absolute;
+ position: sticky !important;
+ bottom: 0 !important;
}
-/* Expand hover area to reach color pickers */
-.urdf-gui .cr.color .c:hover {
- padding: 15px 0;
- margin: -15px 0;
+.urdf-gui li.cr.number.has-slider .property-name:hover {
+ overflow-x: auto;
+ text-overflow: unset;
}
-.urdf-gui li.cr.number.has-slider .property-name {
- flex: 1 1;
- padding-right: 15px;
+.urdf-gui li.cr.string .c,
+.urdf-gui li.cr.color .c {
+ flex: 0 1 180px;
+ margin-left: auto;
}
-.urdf-gui li.cr.number.has-slider .property-name:hover {
- overflow-x: auto;
- text-overflow: unset;
+/* Expand hover area to reach color pickers */
+.urdf-gui .cr.color .c:hover {
+ padding: 15px 0;
+ margin: -15px 0;
}
-/* Adjust the control container */
.urdf-gui li.cr.number.has-slider .c {
flex: 0 1 180px;
margin-left: auto;
}
+li.cr.color > div,
+li.cr.string > div,
li.cr.number.has-slider > div {
display: flex;
}