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; }