git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@10544 75d07b2b-3a1a-0410-a2c5-0572b91ccdca3.0
parent
c972861331
commit
3195940994
@ -1,350 +1,350 @@ |
|||||||
/* |
/* |
||||||
* Copyright (c) 2009-2012 jMonkeyEngine |
* Copyright (c) 2009-2012 jMonkeyEngine |
||||||
* All rights reserved. |
* All rights reserved. |
||||||
* |
* |
||||||
* Redistribution and use in source and binary forms, with or without |
* Redistribution and use in source and binary forms, with or without |
||||||
* modification, are permitted provided that the following conditions are |
* modification, are permitted provided that the following conditions are |
||||||
* met: |
* met: |
||||||
* |
* |
||||||
* * Redistributions of source code must retain the above copyright |
* * Redistributions of source code must retain the above copyright |
||||||
* notice, this list of conditions and the following disclaimer. |
* notice, this list of conditions and the following disclaimer. |
||||||
* |
* |
||||||
* * Redistributions in binary form must reproduce the above copyright |
* * Redistributions in binary form must reproduce the above copyright |
||||||
* notice, this list of conditions and the following disclaimer in the |
* notice, this list of conditions and the following disclaimer in the |
||||||
* documentation and/or other materials provided with the distribution. |
* documentation and/or other materials provided with the distribution. |
||||||
* |
* |
||||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors |
||||||
* may be used to endorse or promote products derived from this software |
* may be used to endorse or promote products derived from this software |
||||||
* without specific prior written permission. |
* without specific prior written permission. |
||||||
* |
* |
||||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
||||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
||||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
||||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
||||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
||||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
||||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
||||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
||||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
||||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||||
*/ |
*/ |
||||||
package com.jme3.shader; |
package com.jme3.shader; |
||||||
|
|
||||||
import com.jme3.math.*; |
import com.jme3.math.*; |
||||||
import com.jme3.util.BufferUtils; |
import com.jme3.util.BufferUtils; |
||||||
import java.nio.FloatBuffer; |
import java.nio.FloatBuffer; |
||||||
import java.nio.IntBuffer; |
import java.nio.IntBuffer; |
||||||
|
|
||||||
public class Uniform extends ShaderVariable { |
public class Uniform extends ShaderVariable { |
||||||
|
|
||||||
private static final Integer ZERO_INT = Integer.valueOf(0); |
private static final Integer ZERO_INT = Integer.valueOf(0); |
||||||
private static final Float ZERO_FLT = Float.valueOf(0); |
private static final Float ZERO_FLT = Float.valueOf(0); |
||||||
private static final FloatBuffer ZERO_BUF = BufferUtils.createFloatBuffer(4*4); |
private static final FloatBuffer ZERO_BUF = BufferUtils.createFloatBuffer(4*4); |
||||||
|
|
||||||
/** |
/** |
||||||
* Currently set value of the uniform. |
* Currently set value of the uniform. |
||||||
*/ |
*/ |
||||||
protected Object value = null; |
protected Object value = null; |
||||||
|
|
||||||
/** |
/** |
||||||
* For arrays or matrices, efficient format |
* For arrays or matrices, efficient format |
||||||
* that can be sent to GL faster. |
* that can be sent to GL faster. |
||||||
*/ |
*/ |
||||||
protected FloatBuffer multiData = null; |
protected FloatBuffer multiData = null; |
||||||
|
|
||||||
/** |
/** |
||||||
* Type of uniform |
* Type of uniform |
||||||
*/ |
*/ |
||||||
protected VarType varType; |
protected VarType varType; |
||||||
|
|
||||||
/** |
/** |
||||||
* Binding to a renderer value, or null if user-defined uniform |
* Binding to a renderer value, or null if user-defined uniform |
||||||
*/ |
*/ |
||||||
protected UniformBinding binding; |
protected UniformBinding binding; |
||||||
|
|
||||||
/** |
/** |
||||||
* Used to track which uniforms to clear to avoid |
* Used to track which uniforms to clear to avoid |
||||||
* values leaking from other materials that use that shader. |
* values leaking from other materials that use that shader. |
||||||
*/ |
*/ |
||||||
protected boolean setByCurrentMaterial = false; |
protected boolean setByCurrentMaterial = false; |
||||||
|
|
||||||
@Override |
@Override |
||||||
public String toString(){ |
public String toString(){ |
||||||
StringBuilder sb = new StringBuilder(); |
StringBuilder sb = new StringBuilder(); |
||||||
sb.append("Uniform[name="); |
sb.append("Uniform[name="); |
||||||
sb.append(name); |
sb.append(name); |
||||||
if (varType != null){ |
if (varType != null){ |
||||||
sb.append(", type="); |
sb.append(", type="); |
||||||
sb.append(varType); |
sb.append(varType); |
||||||
sb.append(", value="); |
sb.append(", value="); |
||||||
sb.append(value); |
sb.append(value); |
||||||
}else{ |
}else{ |
||||||
sb.append(", value=<not set>"); |
sb.append(", value=<not set>"); |
||||||
} |
} |
||||||
sb.append("]"); |
sb.append("]"); |
||||||
return sb.toString(); |
return sb.toString(); |
||||||
} |
} |
||||||
|
|
||||||
public void setBinding(UniformBinding binding){ |
public void setBinding(UniformBinding binding){ |
||||||
this.binding = binding; |
this.binding = binding; |
||||||
} |
} |
||||||
|
|
||||||
public UniformBinding getBinding(){ |
public UniformBinding getBinding(){ |
||||||
return binding; |
return binding; |
||||||
} |
} |
||||||
|
|
||||||
public VarType getVarType() { |
public VarType getVarType() { |
||||||
return varType; |
return varType; |
||||||
} |
} |
||||||
|
|
||||||
public Object getValue(){ |
public Object getValue(){ |
||||||
return value; |
return value; |
||||||
} |
} |
||||||
|
|
||||||
public boolean isSetByCurrentMaterial() { |
public boolean isSetByCurrentMaterial() { |
||||||
return setByCurrentMaterial; |
return setByCurrentMaterial; |
||||||
} |
} |
||||||
|
|
||||||
public void clearSetByCurrentMaterial(){ |
public void clearSetByCurrentMaterial(){ |
||||||
setByCurrentMaterial = false; |
setByCurrentMaterial = false; |
||||||
} |
} |
||||||
|
|
||||||
private static void setVector4(Vector4f vec, Object value) { |
private static void setVector4(Vector4f vec, Object value) { |
||||||
if (value instanceof ColorRGBA) { |
if (value instanceof ColorRGBA) { |
||||||
ColorRGBA color = (ColorRGBA) value; |
ColorRGBA color = (ColorRGBA) value; |
||||||
vec.set(color.r, color.g, color.b, color.a); |
vec.set(color.r, color.g, color.b, color.a); |
||||||
} else if (value instanceof Quaternion) { |
} else if (value instanceof Quaternion) { |
||||||
Quaternion quat = (Quaternion) value; |
Quaternion quat = (Quaternion) value; |
||||||
vec.set(quat.getX(), quat.getY(), quat.getZ(), quat.getW()); |
vec.set(quat.getX(), quat.getY(), quat.getZ(), quat.getW()); |
||||||
} else if (value instanceof Vector4f) { |
} else if (value instanceof Vector4f) { |
||||||
Vector4f vec4 = (Vector4f) value; |
Vector4f vec4 = (Vector4f) value; |
||||||
vec.set(vec4); |
vec.set(vec4); |
||||||
} else{ |
} else{ |
||||||
throw new IllegalArgumentException(); |
throw new IllegalArgumentException(); |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
public void clearValue(){ |
public void clearValue(){ |
||||||
updateNeeded = true; |
updateNeeded = true; |
||||||
|
|
||||||
if (multiData != null){ |
if (multiData != null){ |
||||||
multiData.clear(); |
multiData.clear(); |
||||||
|
|
||||||
while (multiData.remaining() > 0){ |
while (multiData.remaining() > 0){ |
||||||
ZERO_BUF.clear(); |
ZERO_BUF.clear(); |
||||||
ZERO_BUF.limit( Math.min(multiData.remaining(), 16) ); |
ZERO_BUF.limit( Math.min(multiData.remaining(), 16) ); |
||||||
multiData.put(ZERO_BUF); |
multiData.put(ZERO_BUF); |
||||||
} |
} |
||||||
|
|
||||||
multiData.clear(); |
multiData.clear(); |
||||||
|
|
||||||
return; |
return; |
||||||
} |
} |
||||||
|
|
||||||
if (varType == null) { |
if (varType == null) { |
||||||
return; |
return; |
||||||
} |
} |
||||||
|
|
||||||
switch (varType){ |
switch (varType){ |
||||||
case Int: |
case Int: |
||||||
this.value = ZERO_INT; |
this.value = ZERO_INT; |
||||||
break; |
break; |
||||||
case Boolean: |
case Boolean: |
||||||
this.value = Boolean.FALSE; |
this.value = Boolean.FALSE; |
||||||
break; |
break; |
||||||
case Float: |
case Float: |
||||||
this.value = ZERO_FLT; |
this.value = ZERO_FLT; |
||||||
break; |
break; |
||||||
case Vector2: |
case Vector2: |
||||||
this.value = Vector2f.ZERO; |
this.value = Vector2f.ZERO; |
||||||
break; |
break; |
||||||
case Vector3: |
case Vector3: |
||||||
this.value = Vector3f.ZERO; |
this.value = Vector3f.ZERO; |
||||||
break; |
break; |
||||||
case Vector4: |
case Vector4: |
||||||
this.value = Vector4f.ZERO; |
this.value = Vector4f.ZERO; |
||||||
break; |
break; |
||||||
default: |
default: |
||||||
// won't happen because those are either textures
|
// won't happen because those are either textures
|
||||||
// or multidata types
|
// or multidata types
|
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
public void setValue(VarType type, Object value){ |
public void setValue(VarType type, Object value){ |
||||||
if (location == LOC_NOT_DEFINED) { |
if (location == LOC_NOT_DEFINED) { |
||||||
return; |
return; |
||||||
} |
} |
||||||
|
|
||||||
if (varType != null && varType != type) { |
if (varType != null && varType != type) { |
||||||
throw new IllegalArgumentException("Expected a " + varType.name() + " value!"); |
throw new IllegalArgumentException("Expected a " + varType.name() + " value!"); |
||||||
} |
} |
||||||
|
|
||||||
if (value == null) { |
if (value == null) { |
||||||
throw new NullPointerException(); |
throw new NullPointerException(); |
||||||
} |
} |
||||||
|
|
||||||
setByCurrentMaterial = true; |
setByCurrentMaterial = true; |
||||||
|
|
||||||
switch (type){ |
switch (type){ |
||||||
case Matrix3: |
case Matrix3: |
||||||
Matrix3f m3 = (Matrix3f) value; |
Matrix3f m3 = (Matrix3f) value; |
||||||
if (multiData == null) { |
if (multiData == null) { |
||||||
multiData = BufferUtils.createFloatBuffer(9); |
multiData = BufferUtils.createFloatBuffer(9); |
||||||
} |
} |
||||||
m3.fillFloatBuffer(multiData, true); |
m3.fillFloatBuffer(multiData, true); |
||||||
multiData.clear(); |
multiData.clear(); |
||||||
break; |
break; |
||||||
case Matrix4: |
case Matrix4: |
||||||
Matrix4f m4 = (Matrix4f) value; |
Matrix4f m4 = (Matrix4f) value; |
||||||
if (multiData == null) { |
if (multiData == null) { |
||||||
multiData = BufferUtils.createFloatBuffer(16); |
multiData = BufferUtils.createFloatBuffer(16); |
||||||
} |
} |
||||||
m4.fillFloatBuffer(multiData, true); |
m4.fillFloatBuffer(multiData, true); |
||||||
multiData.clear(); |
multiData.clear(); |
||||||
break; |
break; |
||||||
case IntArray: |
case IntArray: |
||||||
int[] ia = (int[]) value; |
int[] ia = (int[]) value; |
||||||
if (this.value == null) { |
if (this.value == null) { |
||||||
this.value = BufferUtils.createIntBuffer(ia); |
this.value = BufferUtils.createIntBuffer(ia); |
||||||
} else { |
} else { |
||||||
this.value = BufferUtils.ensureLargeEnough((IntBuffer)this.value, ia.length); |
this.value = BufferUtils.ensureLargeEnough((IntBuffer)this.value, ia.length); |
||||||
} |
} |
||||||
((IntBuffer)this.value).clear(); |
((IntBuffer)this.value).clear(); |
||||||
break; |
break; |
||||||
case FloatArray: |
case FloatArray: |
||||||
float[] fa = (float[]) value; |
float[] fa = (float[]) value; |
||||||
if (multiData == null) { |
if (multiData == null) { |
||||||
multiData = BufferUtils.createFloatBuffer(fa); |
multiData = BufferUtils.createFloatBuffer(fa); |
||||||
} else { |
} else { |
||||||
multiData = BufferUtils.ensureLargeEnough(multiData, fa.length); |
multiData = BufferUtils.ensureLargeEnough(multiData, fa.length); |
||||||
} |
} |
||||||
multiData.put(fa); |
multiData.put(fa); |
||||||
multiData.clear(); |
multiData.clear(); |
||||||
break; |
break; |
||||||
case Vector2Array: |
case Vector2Array: |
||||||
Vector2f[] v2a = (Vector2f[]) value; |
Vector2f[] v2a = (Vector2f[]) value; |
||||||
if (multiData == null) { |
if (multiData == null) { |
||||||
multiData = BufferUtils.createFloatBuffer(v2a); |
multiData = BufferUtils.createFloatBuffer(v2a); |
||||||
} else { |
} else { |
||||||
multiData = BufferUtils.ensureLargeEnough(multiData, v2a.length * 2); |
multiData = BufferUtils.ensureLargeEnough(multiData, v2a.length * 2); |
||||||
} |
} |
||||||
for (int i = 0; i < v2a.length; i++) { |
for (int i = 0; i < v2a.length; i++) { |
||||||
BufferUtils.setInBuffer(v2a[i], multiData, i); |
BufferUtils.setInBuffer(v2a[i], multiData, i); |
||||||
} |
} |
||||||
multiData.clear(); |
multiData.clear(); |
||||||
break; |
break; |
||||||
case Vector3Array: |
case Vector3Array: |
||||||
Vector3f[] v3a = (Vector3f[]) value; |
Vector3f[] v3a = (Vector3f[]) value; |
||||||
if (multiData == null) { |
if (multiData == null) { |
||||||
multiData = BufferUtils.createFloatBuffer(v3a); |
multiData = BufferUtils.createFloatBuffer(v3a); |
||||||
} else { |
} else { |
||||||
multiData = BufferUtils.ensureLargeEnough(multiData, v3a.length * 3); |
multiData = BufferUtils.ensureLargeEnough(multiData, v3a.length * 3); |
||||||
} |
} |
||||||
for (int i = 0; i < v3a.length; i++) { |
for (int i = 0; i < v3a.length; i++) { |
||||||
BufferUtils.setInBuffer(v3a[i], multiData, i); |
BufferUtils.setInBuffer(v3a[i], multiData, i); |
||||||
} |
} |
||||||
multiData.clear(); |
multiData.clear(); |
||||||
break; |
break; |
||||||
case Vector4Array: |
case Vector4Array: |
||||||
Vector4f[] v4a = (Vector4f[]) value; |
Vector4f[] v4a = (Vector4f[]) value; |
||||||
if (multiData == null) { |
if (multiData == null) { |
||||||
multiData = BufferUtils.createFloatBuffer(v4a); |
multiData = BufferUtils.createFloatBuffer(v4a); |
||||||
} else { |
} else { |
||||||
multiData = BufferUtils.ensureLargeEnough(multiData, v4a.length * 4); |
multiData = BufferUtils.ensureLargeEnough(multiData, v4a.length * 4); |
||||||
} |
} |
||||||
for (int i = 0; i < v4a.length; i++) { |
for (int i = 0; i < v4a.length; i++) { |
||||||
BufferUtils.setInBuffer(v4a[i], multiData, i); |
BufferUtils.setInBuffer(v4a[i], multiData, i); |
||||||
} |
} |
||||||
multiData.clear(); |
multiData.clear(); |
||||||
break; |
break; |
||||||
case Matrix3Array: |
case Matrix3Array: |
||||||
Matrix3f[] m3a = (Matrix3f[]) value; |
Matrix3f[] m3a = (Matrix3f[]) value; |
||||||
if (multiData == null) { |
if (multiData == null) { |
||||||
multiData = BufferUtils.createFloatBuffer(m3a.length * 9); |
multiData = BufferUtils.createFloatBuffer(m3a.length * 9); |
||||||
} else { |
} else { |
||||||
multiData = BufferUtils.ensureLargeEnough(multiData, m3a.length * 9); |
multiData = BufferUtils.ensureLargeEnough(multiData, m3a.length * 9); |
||||||
} |
} |
||||||
for (int i = 0; i < m3a.length; i++) { |
for (int i = 0; i < m3a.length; i++) { |
||||||
m3a[i].fillFloatBuffer(multiData, true); |
m3a[i].fillFloatBuffer(multiData, true); |
||||||
} |
} |
||||||
multiData.clear(); |
multiData.clear(); |
||||||
break; |
break; |
||||||
case Matrix4Array: |
case Matrix4Array: |
||||||
Matrix4f[] m4a = (Matrix4f[]) value; |
Matrix4f[] m4a = (Matrix4f[]) value; |
||||||
if (multiData == null) { |
if (multiData == null) { |
||||||
multiData = BufferUtils.createFloatBuffer(m4a.length * 16); |
multiData = BufferUtils.createFloatBuffer(m4a.length * 16); |
||||||
} else { |
} else { |
||||||
multiData = BufferUtils.ensureLargeEnough(multiData, m4a.length * 16); |
multiData = BufferUtils.ensureLargeEnough(multiData, m4a.length * 16); |
||||||
} |
} |
||||||
for (int i = 0; i < m4a.length; i++) { |
for (int i = 0; i < m4a.length; i++) { |
||||||
m4a[i].fillFloatBuffer(multiData, true); |
m4a[i].fillFloatBuffer(multiData, true); |
||||||
} |
} |
||||||
multiData.clear(); |
multiData.clear(); |
||||||
break; |
break; |
||||||
// Only use check if equals optimization for primitive values
|
// Only use check if equals optimization for primitive values
|
||||||
case Int: |
case Int: |
||||||
case Float: |
case Float: |
||||||
case Boolean: |
case Boolean: |
||||||
if (this.value != null && this.value.equals(value)) { |
if (this.value != null && this.value.equals(value)) { |
||||||
return; |
return; |
||||||
} |
} |
||||||
this.value = value; |
this.value = value; |
||||||
break; |
break; |
||||||
default: |
default: |
||||||
this.value = value; |
this.value = value; |
||||||
break; |
break; |
||||||
} |
} |
||||||
|
|
||||||
if (multiData != null) { |
if (multiData != null) { |
||||||
this.value = multiData; |
this.value = multiData; |
||||||
} |
} |
||||||
|
|
||||||
varType = type; |
varType = type; |
||||||
updateNeeded = true; |
updateNeeded = true; |
||||||
} |
} |
||||||
|
|
||||||
public void setVector4Length(int length){ |
public void setVector4Length(int length){ |
||||||
if (location == -1) |
if (location == -1) |
||||||
return; |
return; |
||||||
|
|
||||||
FloatBuffer fb = (FloatBuffer) value; |
FloatBuffer fb = (FloatBuffer) value; |
||||||
if (fb == null || fb.capacity() < length) { |
if (fb == null || fb.capacity() < length * 4) { |
||||||
value = BufferUtils.createFloatBuffer(length * 4); |
value = BufferUtils.createFloatBuffer(length * 4); |
||||||
} |
} |
||||||
|
|
||||||
varType = VarType.Vector4Array; |
varType = VarType.Vector4Array; |
||||||
updateNeeded = true; |
updateNeeded = true; |
||||||
setByCurrentMaterial = true; |
setByCurrentMaterial = true; |
||||||
} |
} |
||||||
|
|
||||||
public void setVector4InArray(float x, float y, float z, float w, int index){ |
public void setVector4InArray(float x, float y, float z, float w, int index){ |
||||||
if (location == -1) |
if (location == -1) |
||||||
return; |
return; |
||||||
|
|
||||||
if (varType != null && varType != VarType.Vector4Array) |
if (varType != null && varType != VarType.Vector4Array) |
||||||
throw new IllegalArgumentException("Expected a "+varType.name()+" value!"); |
throw new IllegalArgumentException("Expected a "+varType.name()+" value!"); |
||||||
|
|
||||||
FloatBuffer fb = (FloatBuffer) value; |
FloatBuffer fb = (FloatBuffer) value; |
||||||
fb.position(index * 4); |
fb.position(index * 4); |
||||||
fb.put(x).put(y).put(z).put(w); |
fb.put(x).put(y).put(z).put(w); |
||||||
fb.rewind(); |
fb.rewind(); |
||||||
updateNeeded = true; |
updateNeeded = true; |
||||||
setByCurrentMaterial = true; |
setByCurrentMaterial = true; |
||||||
} |
} |
||||||
|
|
||||||
public boolean isUpdateNeeded(){ |
public boolean isUpdateNeeded(){ |
||||||
return updateNeeded; |
return updateNeeded; |
||||||
} |
} |
||||||
|
|
||||||
public void clearUpdateNeeded(){ |
public void clearUpdateNeeded(){ |
||||||
updateNeeded = false; |
updateNeeded = false; |
||||||
} |
} |
||||||
|
|
||||||
public void reset(){ |
public void reset(){ |
||||||
setByCurrentMaterial = false; |
setByCurrentMaterial = false; |
||||||
location = -2; |
location = -2; |
||||||
updateNeeded = true; |
updateNeeded = true; |
||||||
} |
} |
||||||
|
|
||||||
} |
} |
||||||
|
Loading…
Reference in new issue