diff --git a/resources/web/wwi/Parser.js b/resources/web/wwi/Parser.js index c54213955ff..bea4d58cdb1 100644 --- a/resources/web/wwi/Parser.js +++ b/resources/web/wwi/Parser.js @@ -674,9 +674,18 @@ export default class Parser { newNode = new WbGyro(id, translation, scale, rotation, name === '' ? 'gyro' : name); else if (node.tagName === 'InertialUnit') newNode = new WbInertialUnit(id, translation, scale, rotation, name === '' ? 'inertial unit' : name); - else if (node.tagName === 'LED') - newNode = new WbLed(id, translation, scale, rotation, name === '' ? 'led' : name); - else if (node.tagName === 'Lidar') { + else if (node.tagName === 'LED') { + let tempColor = getNodeAttribute(node, 'color', '1 0 0'); + tempColor = convertStringToFloatArray(tempColor); + const color = []; + if (tempColor.length % 3 === 0) { + for (let i = 0; i < tempColor.length; i += 3) + color.push(new WbVector3(tempColor[i], tempColor[i + 1], tempColor[i + 2])); + } else + console.error('Wrong number of colors in LED'); + + newNode = new WbLed(id, translation, scale, rotation, name === '' ? 'led' : name, color); + } else if (node.tagName === 'Lidar') { const fieldOfView = parseFloat(getNodeAttribute(node, 'fieldOfView', Math.PI / 2)); const horizontalResolution = parseInt(getNodeAttribute(node, 'horizontalResolution', '512')); const maxRange = parseFloat(getNodeAttribute(node, 'maxRange', '1')); diff --git a/resources/web/wwi/nodes/WbAppearance.js b/resources/web/wwi/nodes/WbAppearance.js index 2eabaf499ca..106bf10dc2b 100644 --- a/resources/web/wwi/nodes/WbAppearance.js +++ b/resources/web/wwi/nodes/WbAppearance.js @@ -24,6 +24,9 @@ export default class WbAppearance extends WbAbstractAppearance { this.#material.onChange = () => this.#update(); this.#update(); + + if (typeof this.notifyLed !== 'undefined') + this.notifyLed(); } get texture() { diff --git a/resources/web/wwi/nodes/WbLed.js b/resources/web/wwi/nodes/WbLed.js index c8edb745577..c792363c02d 100644 --- a/resources/web/wwi/nodes/WbLed.js +++ b/resources/web/wwi/nodes/WbLed.js @@ -1,5 +1,160 @@ +import WbAppearance from './WbAppearance.js'; import WbDevice from './WbDevice.js'; +import WbGroup from './WbGroup.js'; +import WbLight from './WbLight.js'; +import WbShape from './WbShape.js'; +import WbVector3 from './utils/WbVector3.js'; +import {clampValuesIfNeeded} from './utils/WbFieldChecker.js'; // This class is used to retrieve the type of device export default class WbLed extends WbDevice { + #color; + #lights; + #materials; + #pbrAppearances; + #value; + constructor(id, translation, scale, rotation, name, color) { + super(id, translation, scale, rotation, name); + this.#lights = []; + this.#materials = []; + this.#pbrAppearances = []; + this.#color = color; + this.#value = 0; + } + + get value() { + return this.#value; + } + + set value(newValue) { + if (this.#value === newValue) + return; + this.#value = newValue; + + this.#setMaterialsAndLightsColor(); + } + + postFinalize() { + super.postFinalize(); + + this._updateChildren(); + + // disable WREN lights if not on + const on = this.#value !== 0; + for (const light of this.#lights) + light.on = on; + } + + applyOptionalRendering(enable) { + this.value = enable ? 1 : 0; + } + + _updateChildren() { + if (!this.isPostFinalizedCalled) + return; + + this.#findMaterialsAndLights(this); + + // update color of lights and materials + this.#setMaterialsAndLightsColor(); + } + + #clearMaterialsAndLights() { + this.#materials = []; + this.#lights = []; + this.#pbrAppearances = []; + } + + #findMaterialsAndLights(group) { + let size = group.children.length; + if (size < 1) + return; + + if (group === this) { + this.#clearMaterialsAndLights(); + size = 1; // we look only into the first child of the WbLed node + } + + for (const child of group.children) { + if (child instanceof WbShape && typeof child.appearance !== 'undefined') { + const appearance = child.appearance; + if (appearance instanceof WbAppearance) { + const material = appearance.material; + if (typeof material !== 'undefined') + this.#materials.push(material); + + appearance.notifyLed = () => this._updateChildren(); + child.notifyLed = () => this._updateChildren(); + } else if (typeof appearance !== 'undefined') { + this.#pbrAppearances.push(appearance); + child.notifyLed = () => this._updateChildren(); + } + } else if (child instanceof WbLight) + this.#lights.push(child); + else if (child instanceof WbGroup) { + this.#findMaterialsAndLights(child); + const proxy = new Proxy(child.children, { + deleteProperty: (target, property) => { + delete target[property]; + this._updateChildren(); + return true; + }, + set: (target, property, value, receiver) => { + target[property] = value; + this._updateChildren(); + return true; + } + }); + child.children = proxy; + } + } + + if (group === this && !this.#isAnyMaterialOrLightFound()) + console.warn(`No PBRAppearance, Material and no Light found. + The first child of a LED should be either a Shape, a Light or a Group containing Shape and Light nodes.`); + } + + #isAnyMaterialOrLightFound() { + return (this.#materials.length > 0 || this.#lights.length > 0 || this.#pbrAppearances.length > 0); + } + + #setMaterialsAndLightsColor() { + // compute the new color + let r, g, b; + if (this.#value > 0) { + if (this.#value - 1 < this.#color.length) { + const c = this.#color[this.value - 1]; + r = c.x; + g = c.y; + b = c.z; + } else { + r = 1; + g = 1; + b = 1; + } + } else { + r = 0; + g = 0; + b = 0; + } + + let color = new WbVector3(r, g, b); + color = clampValuesIfNeeded(color); + + // update every material + for (const material of this.#materials) + material.emissiveColor = color; + + // same for PbrAppearances + for (const pbrAppearance of this.#pbrAppearances) + pbrAppearance.emissiveColor = color; + + // update every lights + const on = this.#value !== 0; + for (const light of this.#lights) { + light.color = color; + // disable WREN lights if not on + light.on = on; + } + } } diff --git a/resources/web/wwi/nodes/WbShape.js b/resources/web/wwi/nodes/WbShape.js index fb6ad162f0e..d7edd3fc0fd 100644 --- a/resources/web/wwi/nodes/WbShape.js +++ b/resources/web/wwi/nodes/WbShape.js @@ -42,6 +42,9 @@ export default class WbShape extends WbBaseNode { useNode.appearance.parent = useNode.id; } } + + if (typeof this.notifyLed !== 'undefined') + this.notifyLed(); } applyMaterialToGeometry() { diff --git a/resources/web/wwi/nodes/utils/WbFieldChecker.js b/resources/web/wwi/nodes/utils/WbFieldChecker.js index e7b20cccfb3..11f9ab72f72 100644 --- a/resources/web/wwi/nodes/utils/WbFieldChecker.js +++ b/resources/web/wwi/nodes/utils/WbFieldChecker.js @@ -86,5 +86,5 @@ function clampValue(value) { return value; } -export {resetIfNegative, resetIfNonPositive, resetVector2IfNonPositive, resetVector3IfNonPositive, +export {resetIfNegative, resetIfNonPositive, resetVector2IfNonPositive, resetVector3IfNonPositive, clampValuesIfNeeded, resetIfNotInRangeWithIncludedBounds, resetColorIfInvalid, resetMultipleColorIfInvalid, resetVector3IfNegative}; diff --git a/resources/web/wwi/protoVisualizer/Node.js b/resources/web/wwi/protoVisualizer/Node.js index 8687828a421..bd5ce90f759 100644 --- a/resources/web/wwi/protoVisualizer/Node.js +++ b/resources/web/wwi/protoVisualizer/Node.js @@ -288,7 +288,7 @@ export default class Node { if (line.indexOf('EXTERNPROTO') !== -1) { // get only the text after 'EXTERNPROTO' let address = line.split('EXTERNPROTO')[1].trim().replaceAll('"', ''); if (address.startsWith('webots://')) - address = 'https://raw.githubusercontent.com/cyberbotics/webots/released/' + address.substring(9); + address = 'https://raw.githubusercontent.com/cyberbotics/webots/develop/' + address.substring(9); // TODO change to released once a new version is published else address = combinePaths(address, protoUrl); diff --git a/resources/web/wwi/protoVisualizer/Vrml.js b/resources/web/wwi/protoVisualizer/Vrml.js index 450e4d01f8c..07524da5b4c 100644 --- a/resources/web/wwi/protoVisualizer/Vrml.js +++ b/resources/web/wwi/protoVisualizer/Vrml.js @@ -728,6 +728,9 @@ export class MFVec3f extends MultipleValue { export class MFColor extends MultipleValue { setValueFromTokenizer(tokenizer) { + // If we reach this function it means that the MFColor does not have the default value. + // Thus we should reset the value because it already contains the default one: [1 0 0]. + this.value = []; if (tokenizer.peekWord() === '[') { tokenizer.skipToken('[');