[DEV] initial install

This commit is contained in:
Edouard DUPIN 2021-05-03 17:01:35 +02:00
commit f1722f6755
15 changed files with 746 additions and 0 deletions

25
.classpath Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" path="src"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER">
<attributes>
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
<classpathentry combineaccessrules="false" exported="true" kind="src" path="/scenarium-logger">
<attributes>
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
<classpathentry combineaccessrules="false" exported="true" kind="src" path="/atriasoft-etk">
<attributes>
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
<classpathentry combineaccessrules="false" kind="src" path="/atriasoft-gale">
<attributes>
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="out/eclipse/"/>
</classpath>

17
.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>loader3d</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>

7
README.md Normal file
View File

@ -0,0 +1,7 @@
External loader of 3D model
===========================
Licence MPL-2

10
src/module-info.java Normal file
View File

@ -0,0 +1,10 @@
module org.atriasoft.loader3d {
exports org.atriasoft.loader3d;
exports org.atriasoft.loader3d.model;
exports org.atriasoft.loader3d.resources;
requires transitive org.atriasoft.etk;
requires transitive org.atriasoft.gale;
}

View File

@ -0,0 +1,11 @@
package org.atriasoft.loader3d;
public class Loader3d {
/*
public static RawModel loadObjModel(final Uri fileName, final Loader loader) {
System.out.println("Load file " + fileName);
final ModelData data = OBJFileLoader.loadOBJ(fileName);
return loader.loadToVAO(data.getVertices(), data.getTextureCoords(), data.getNormals(), data.getIndices());
}
*/
}

View File

@ -0,0 +1,166 @@
package org.atriasoft.loader3d;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import org.atriasoft.etk.Uri;
import org.atriasoft.etk.math.Vector2f;
import org.atriasoft.etk.math.Vector3f;
import org.atriasoft.loader3d.internal.Log;
import org.atriasoft.loader3d.model.ModelData;
import org.atriasoft.loader3d.model.Vertex;
public class OBJFileLoader {
private static float convertDataToArrays(final List<Vertex> vertices, final List<Vector2f> textures, final List<Vector3f> normals, final float[] verticesArray, final float[] texturesArray,
final float[] normalsArray) {
float furthestPoint = 0;
for (int i = 0; i < vertices.size(); i++) {
final Vertex currentVertex = vertices.get(i);
if (currentVertex.getLength() > furthestPoint) {
furthestPoint = currentVertex.getLength();
}
final Vector3f position = currentVertex.getPosition();
final Vector2f textureCoord = textures.get(currentVertex.getTextureIndex());
final Vector3f normalVector = normals.get(currentVertex.getNormalIndex());
verticesArray[i * 3] = position.x();
verticesArray[i * 3 + 1] = position.y();
verticesArray[i * 3 + 2] = position.z();
texturesArray[i * 2] = textureCoord.x();
texturesArray[i * 2 + 1] = 1 - textureCoord.y();
normalsArray[i * 3] = normalVector.x();
normalsArray[i * 3 + 1] = normalVector.y();
normalsArray[i * 3 + 2] = normalVector.z();
}
return furthestPoint;
}
private static int[] convertIndicesListToArray(final List<Integer> indices) {
final int[] indicesArray = new int[indices.size()];
for (int i = 0; i < indicesArray.length; i++) {
indicesArray[i] = indices.get(i);
}
return indicesArray;
}
private static void dealWithAlreadyProcessedVertex(final Vertex previousVertex, final int newTextureIndex, final int newNormalIndex, final List<Integer> indices, final List<Vertex> vertices) {
if (previousVertex.hasSameTextureAndNormal(newTextureIndex, newNormalIndex)) {
indices.add(previousVertex.getIndex());
} else {
final Vertex anotherVertex = previousVertex.getDuplicateVertex();
if (anotherVertex != null) {
OBJFileLoader.dealWithAlreadyProcessedVertex(anotherVertex, newTextureIndex, newNormalIndex, indices, vertices);
} else {
final Vertex duplicateVertex = new Vertex(vertices.size(), previousVertex.getPosition());
duplicateVertex.setTextureIndex(newTextureIndex);
duplicateVertex.setNormalIndex(newNormalIndex);
previousVertex.setDuplicateVertex(duplicateVertex);
vertices.add(duplicateVertex);
indices.add(duplicateVertex.getIndex());
}
}
}
public static ModelData loadOBJ(final Uri objFileName) {
final InputStream stream = Uri.getStream(objFileName);
if (stream == null) {
Log.error("Can not read file: " + objFileName + " ==> load OBJ");
return null;
}
/*
final FileReader isr = null;
final File objFile = new File(objFileName);
try {
isr = new FileReader(objFile);
} catch (final FileNotFoundException e) {
System.err.println("File not found in res; don't use any extention");
return null;
}
*/
BufferedReader reader;
try {
reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
} catch (final UnsupportedEncodingException e1) {
Log.error("Error in loading file: " + objFileName + " ==> load OBJ");
e1.printStackTrace();
return null;
}
String line;
final List<Vertex> vertices = new ArrayList<>();
final List<Vector2f> textures = new ArrayList<>();
final List<Vector3f> normals = new ArrayList<>();
final List<Integer> indices = new ArrayList<>();
try {
while (true) {
line = reader.readLine();
if (line.startsWith("v ")) {
final String[] currentLine = line.split(" ");
final Vector3f vertex = new Vector3f(Float.parseFloat(currentLine[1]), Float.parseFloat(currentLine[2]), Float.parseFloat(currentLine[3]));
final Vertex newVertex = new Vertex(vertices.size(), vertex);
vertices.add(newVertex);
} else if (line.startsWith("vt ")) {
final String[] currentLine = line.split(" ");
final Vector2f texture = new Vector2f(Float.parseFloat(currentLine[1]), Float.parseFloat(currentLine[2]));
textures.add(texture);
} else if (line.startsWith("vn ")) {
final String[] currentLine = line.split(" ");
final Vector3f normal = new Vector3f(Float.parseFloat(currentLine[1]), Float.parseFloat(currentLine[2]), Float.parseFloat(currentLine[3]));
normals.add(normal);
} else if (line.startsWith("f ")) {
break;
}
}
while (line != null && line.startsWith("f ")) {
final String[] currentLine = line.split(" ");
final String[] vertex1 = currentLine[1].split("/");
final String[] vertex2 = currentLine[2].split("/");
final String[] vertex3 = currentLine[3].split("/");
OBJFileLoader.processVertex(vertex1, vertices, indices);
OBJFileLoader.processVertex(vertex2, vertices, indices);
OBJFileLoader.processVertex(vertex3, vertices, indices);
line = reader.readLine();
}
reader.close();
} catch (final IOException e) {
System.err.println("Error reading the file");
}
OBJFileLoader.removeUnusedVertices(vertices);
final float[] verticesArray = new float[vertices.size() * 3];
final float[] texturesArray = new float[vertices.size() * 2];
final float[] normalsArray = new float[vertices.size() * 3];
final float furthest = OBJFileLoader.convertDataToArrays(vertices, textures, normals, verticesArray, texturesArray, normalsArray);
final int[] indicesArray = OBJFileLoader.convertIndicesListToArray(indices);
final ModelData data = new ModelData(verticesArray, texturesArray, normalsArray, indicesArray, furthest);
return data;
}
private static void processVertex(final String[] vertex, final List<Vertex> vertices, final List<Integer> indices) {
final int index = Integer.parseInt(vertex[0]) - 1;
final Vertex currentVertex = vertices.get(index);
final int textureIndex = Integer.parseInt(vertex[1]) - 1;
final int normalIndex = Integer.parseInt(vertex[2]) - 1;
if (!currentVertex.isSet()) {
currentVertex.setTextureIndex(textureIndex);
currentVertex.setNormalIndex(normalIndex);
indices.add(index);
} else {
OBJFileLoader.dealWithAlreadyProcessedVertex(currentVertex, textureIndex, normalIndex, indices, vertices);
}
}
private static void removeUnusedVertices(final List<Vertex> vertices) {
for (final Vertex vertex : vertices) {
if (!vertex.isSet()) {
vertex.setTextureIndex(0);
vertex.setNormalIndex(0);
}
}
}
}

View File

@ -0,0 +1,68 @@
package org.atriasoft.loader3d.internal;
import io.scenarium.logger.LogLevel;
import io.scenarium.logger.Logger;
public class Log {
private static final String LIB_NAME = "loader3d";
private static final String LIB_NAME_DRAW = Logger.getDrawableName(Log.LIB_NAME);
private static final boolean PRINT_CRITICAL = Logger.getNeedPrint(Log.LIB_NAME, LogLevel.CRITICAL);
private static final boolean PRINT_DEBUG = Logger.getNeedPrint(Log.LIB_NAME, LogLevel.DEBUG);
private static final boolean PRINT_ERROR = Logger.getNeedPrint(Log.LIB_NAME, LogLevel.ERROR);
private static final boolean PRINT_INFO = Logger.getNeedPrint(Log.LIB_NAME, LogLevel.INFO);
private static final boolean PRINT_PRINT = Logger.getNeedPrint(Log.LIB_NAME, LogLevel.PRINT);
private static final boolean PRINT_TODO = Logger.getNeedPrint(Log.LIB_NAME, LogLevel.TODO);
private static final boolean PRINT_VERBOSE = Logger.getNeedPrint(Log.LIB_NAME, LogLevel.VERBOSE);
private static final boolean PRINT_WARNING = Logger.getNeedPrint(Log.LIB_NAME, LogLevel.WARNING);
public static void critical(final String data) {
if (Log.PRINT_CRITICAL) {
Logger.critical(Log.LIB_NAME_DRAW, data);
}
}
public static void debug(final String data) {
if (Log.PRINT_DEBUG) {
Logger.debug(Log.LIB_NAME_DRAW, data);
}
}
public static void error(final String data) {
if (Log.PRINT_ERROR) {
Logger.error(Log.LIB_NAME_DRAW, data);
}
}
public static void info(final String data) {
if (Log.PRINT_INFO) {
Logger.info(Log.LIB_NAME_DRAW, data);
}
}
public static void print(final String data) {
if (Log.PRINT_PRINT) {
Logger.print(Log.LIB_NAME_DRAW, data);
}
}
public static void todo(final String data) {
if (Log.PRINT_TODO) {
Logger.todo(Log.LIB_NAME_DRAW, data);
}
}
public static void verbose(final String data) {
if (Log.PRINT_VERBOSE) {
Logger.verbose(Log.LIB_NAME_DRAW, data);
}
}
public static void warning(final String data) {
if (Log.PRINT_WARNING) {
Logger.warning(Log.LIB_NAME_DRAW, data);
}
}
private Log() {}
}

View File

@ -0,0 +1,10 @@
package org.atriasoft.loader3d.model;
public record ModelData(
float[] vertices,
float[] textureCoords,
float[] normals,
int[] indices,
float furthestPoint) {
}

View File

@ -0,0 +1,66 @@
package org.atriasoft.loader3d.model;
import org.atriasoft.etk.math.Vector3f;
public class Vertex {
private static final int NO_INDEX = -1;
private Vector3f position;
private int textureIndex = NO_INDEX;
private int normalIndex = NO_INDEX;
private Vertex duplicateVertex = null;
private int index;
private float length;
public Vertex(int index,Vector3f position){
this.index = index;
this.position = position;
this.length = position.length();
}
public int getIndex(){
return index;
}
public float getLength(){
return length;
}
public boolean isSet(){
return textureIndex!=NO_INDEX && normalIndex!=NO_INDEX;
}
public boolean hasSameTextureAndNormal(int textureIndexOther,int normalIndexOther){
return textureIndexOther==textureIndex && normalIndexOther==normalIndex;
}
public void setTextureIndex(int textureIndex){
this.textureIndex = textureIndex;
}
public void setNormalIndex(int normalIndex){
this.normalIndex = normalIndex;
}
public Vector3f getPosition() {
return position;
}
public int getTextureIndex() {
return textureIndex;
}
public int getNormalIndex() {
return normalIndex;
}
public Vertex getDuplicateVertex() {
return duplicateVertex;
}
public void setDuplicateVertex(Vertex duplicateVertex) {
this.duplicateVertex = duplicateVertex;
}
}

View File

@ -0,0 +1,112 @@
package org.atriasoft.loader3d.resources;
import java.util.ArrayList;
import java.util.List;
import org.atriasoft.etk.Uri;
import org.atriasoft.etk.math.Vector2f;
import org.atriasoft.etk.math.Vector3f;
import org.atriasoft.gale.backend3d.OpenGL.RenderMode;
import org.atriasoft.gale.resource.ResourceVirtualArrayObject;
public class ResourceListTexturedMesh extends ResourceStaticMesh {
public static ResourceListTexturedMesh create(final RenderMode mode) {
ResourceListTexturedMesh resource = new ResourceListTexturedMesh(mode);
getManager().localAdd(resource);
return resource;
}
protected List<Vector3f> vertices = new ArrayList<>();
protected List<Vector2f> textureCoords = new ArrayList<>();
protected List<Vector3f> normals = new ArrayList<>();
protected List<Integer> indices = new ArrayList<>();
protected ResourceListTexturedMesh(final RenderMode mode) {
super(mode);
}
protected ResourceListTexturedMesh(final Uri uriFile) {
super(uriFile);
}
public void addQuad(final Vector3f v1, final Vector3f v2, final Vector3f v3, final Vector3f v4, final Vector2f t1, final Vector2f t2, final Vector2f t3, final Vector2f t4, final Vector3f n1) {
addTriangle(v1, v2, v3, t1, t2, t3, n1, n1, n1);
addTriangle(v1, v3, v4, t1, t3, t4, n1, n1, n1);
}
public void addQuad(final Vector3f v1, final Vector3f v2, final Vector3f v3, final Vector3f v4, final Vector2f t1, final Vector2f t2, final Vector2f t3, final Vector2f t4, final Vector3f n1,
final Vector3f n2, final Vector3f n3, final Vector3f n4) {
addTriangle(v1, v2, v3, t1, t2, t3, n1, n2, n3);
addTriangle(v1, v3, v4, t1, t3, t4, n1, n3, n4);
}
public void addTriangle(final Vector3f v1, final Vector3f v2, final Vector3f v3, final Vector2f t1, final Vector2f t2, final Vector2f t3, final Vector3f n1, final Vector3f n2, final Vector3f n3) {
this.vertices.add(v1);
this.vertices.add(v2);
this.vertices.add(v3);
this.textureCoords.add(t1);
this.textureCoords.add(t2);
this.textureCoords.add(t3);
this.normals.add(n1);
this.normals.add(n2);
this.normals.add(n3);
this.indices.add(this.vertices.size() - 3);
this.indices.add(this.vertices.size() - 2);
this.indices.add(this.vertices.size() - 1);
}
public void clear() {
this.vertices.clear();
this.textureCoords.clear();
this.normals.clear();
this.indices.clear();
}
/**
* Send the data to the graphic card.
*/
public void flush() {
// request to the manager to be call at the next update ...
this.vao = ResourceVirtualArrayObject.create(toFloatV3(this.vertices), toFloatV2(this.textureCoords), toFloatV3(this.normals), toIntArray(this.indices));
this.vao.flush();
}
private float[] toFloatArray(final List<Float> values) {
float[] out = new float[values.size()];
for (int iii = 0; iii < values.size(); iii++) {
out[iii] = values.get(iii);
}
return out;
}
private float[] toFloatV2(final List<Vector2f> values) {
float[] out = new float[values.size() * 2];
for (int iii = 0; iii < values.size(); iii++) {
Vector2f tmp = values.get(iii);
out[iii * 2] = tmp.x();
out[iii * 2 + 1] = tmp.y();
}
return out;
}
private float[] toFloatV3(final List<Vector3f> values) {
float[] out = new float[values.size() * 3];
for (int iii = 0; iii < values.size(); iii++) {
Vector3f tmp = values.get(iii);
out[iii * 3] = tmp.x();
out[iii * 3 + 1] = tmp.y();
out[iii * 3 + 2] = tmp.z();
}
return out;
}
private int[] toIntArray(final List<Integer> values) {
int[] out = new int[values.size()];
for (int iii = 0; iii < values.size(); iii++) {
out[iii] = values.get(iii);
}
return out;
}
}

View File

@ -0,0 +1,42 @@
package org.atriasoft.loader3d.resources;
import org.atriasoft.etk.Uri;
import org.atriasoft.gale.backend3d.OpenGL.RenderMode;
import org.atriasoft.gale.resource.ResourceVirtualArrayObject;
public class ResourceStaticColoredMesh extends ResourceStaticMesh {
public static ResourceStaticColoredMesh create(final float[] vertices, final float[] colors, final float[] normals, final int[] indices, final RenderMode mode) {
ResourceStaticColoredMesh resource = new ResourceStaticColoredMesh(vertices, colors, normals, indices, mode);
getManager().localAdd(resource);
return resource;
}
protected float[] vertices = null;
protected float[] colors = null;
protected float[] normals = null;
protected int[] indices = null;
protected ResourceStaticColoredMesh(final float[] vertices, final float[] colors, final float[] normals, final int[] indices, final RenderMode mode) {
super(mode);
this.vertices = vertices;
this.colors = colors;
this.normals = normals;
this.indices = indices;
flush();
}
protected ResourceStaticColoredMesh(final Uri uriFile) {
super(uriFile);
}
/**
* Send the data to the graphic card.
*/
public void flush() {
// request to the manager to be call at the next update ...
this.vao = ResourceVirtualArrayObject.create(this.vertices, this.colors, null, this.normals, this.indices);
this.vao.flush();
}
}

View File

@ -0,0 +1,54 @@
package org.atriasoft.loader3d.resources;
import org.atriasoft.etk.Uri;
import org.atriasoft.gale.backend3d.OpenGL.RenderMode;
import org.atriasoft.gale.resource.Resource;
import org.atriasoft.gale.resource.ResourceVirtualArrayObject;
public class ResourceStaticMesh extends Resource {
protected RenderMode mode = RenderMode.quadStrip;
protected ResourceVirtualArrayObject vao = null;
protected ResourceStaticMesh(final RenderMode mode) {
this.mode = mode;
}
protected ResourceStaticMesh(final Uri uriFile) {
super(uriFile);
}
public void bindForRendering() {
if (this.vao == null) {
return;
}
this.vao.bindForRendering();
}
@Override
public void cleanUp() {
this.vao.cleanUp();
}
public RenderMode getMode() {
return this.mode;
}
public void render() {
if (this.vao == null) {
return;
}
this.vao.render(this.mode);
}
public void setMode(final RenderMode mode) {
this.mode = mode;
}
public void unBindForRendering() {
if (this.vao == null) {
return;
}
this.vao.unBindForRendering();
}
}

View File

@ -0,0 +1,52 @@
package org.atriasoft.loader3d.resources;
import org.atriasoft.etk.Uri;
import org.atriasoft.gale.backend3d.OpenGL.RenderMode;
import org.atriasoft.gale.resource.Resource;
import org.atriasoft.loader3d.OBJFileLoader;
import org.atriasoft.loader3d.internal.Log;
import org.atriasoft.loader3d.model.ModelData;
public class ResourceStaticMeshObj extends ResourceStaticTexturedMesh {
public static ResourceStaticMeshObj create(final Uri uriObj) {
ResourceStaticMeshObj resource;
Resource resource2;
final String name = uriObj.getValue();
if (name.isEmpty() || name.equals("---")) {
Log.error("Can not create a shader without a filaname");
return null;
}
resource2 = Resource.getManager().localKeep(name);
if (resource2 != null) {
if (resource2 instanceof ResourceStaticMeshObj) {
resource2.keep();
return (ResourceStaticMeshObj) resource2;
}
Log.critical("Request resource file : '" + name + "' With the wrong type (dynamic cast error)");
return null;
}
resource = new ResourceStaticMeshObj(uriObj);
Resource.getManager().localAdd(resource);
return resource;
}
private final Uri uriFile;
public ResourceStaticMeshObj(final Uri uriFile) {
super(uriFile);
this.uriFile = uriFile;
System.out.println("Load file " + uriFile);
final ModelData data = OBJFileLoader.loadOBJ(uriFile);
this.vertices = data.vertices();
this.textureCoords = data.textureCoords();
this.normals = data.normals();
this.indices = data.indices();
this.mode = RenderMode.triangle;
flush();
}
public Uri getUriFile() {
return this.uriFile;
}
}

View File

@ -0,0 +1,64 @@
package org.atriasoft.loader3d.resources;
import org.atriasoft.etk.Uri;
import org.atriasoft.gale.resource.Resource;
import org.atriasoft.gale.resource.ResourceVirtualArrayObject;
import org.atriasoft.loader3d.internal.Log;
public class ResourceStaticMeshObjBynamic extends ResourceStaticMeshObj {
public static ResourceStaticMeshObjBynamic create(final Uri uriObj) {
ResourceStaticMeshObjBynamic resource;
Resource resource2;
final String name = uriObj.getValue();
if (name.isEmpty() || name.equals("---")) {
Log.error("Can not create a shader without a filaname");
return null;
}
resource2 = Resource.getManager().localKeep(name);
if (resource2 != null) {
if (resource2 instanceof ResourceStaticMeshObjBynamic tmpp) {
resource2.keep();
return tmpp;
}
Log.critical("Request resource file : '" + name + "' With the wrong type (dynamic cast error)");
return null;
}
resource = new ResourceStaticMeshObjBynamic(uriObj);
Resource.getManager().localAdd(resource);
return resource;
}
protected ResourceStaticMeshObjBynamic(final Uri uriFile) {
super(uriFile);
}
/**
* Send the data to the graphic card.
*/
@Override
public void flush() {
if (this.vao == null) {
// request to the manager to be call at the next update ...
this.vao = ResourceVirtualArrayObject.createDynamic();
this.vao.setIndices(this.indices);
this.vao.setNormals(this.normals);
this.vao.setPosition(this.vertices);
this.vao.setTextureCoordinate(this.textureCoords);
}
this.vao.flush();
}
public float[] getVertices() {
return this.vertices;
}
public void setVertices(final float[] vertices) {
this.vertices = vertices;
if (this.vao != null) {
this.vao.setPosition(this.vertices);
this.vao.setVertexCount(this.vertices.length);
this.vao.flush();
}
}
}

View File

@ -0,0 +1,42 @@
package org.atriasoft.loader3d.resources;
import org.atriasoft.etk.Uri;
import org.atriasoft.gale.backend3d.OpenGL.RenderMode;
import org.atriasoft.gale.resource.Resource;
import org.atriasoft.gale.resource.ResourceVirtualArrayObject;
public class ResourceStaticTexturedMesh extends ResourceStaticMesh {
public static ResourceStaticTexturedMesh create(final float[] vertices, final float[] textureCoordinates, final float[] normals, final int[] indices, final RenderMode mode) {
ResourceStaticTexturedMesh resource = new ResourceStaticTexturedMesh(vertices, textureCoordinates, normals, indices, mode);
Resource.getManager().localAdd(resource);
return resource;
}
protected int[] indices = null;
protected float[] normals = null;
protected float[] textureCoords = null;
protected float[] vertices = null;
protected ResourceStaticTexturedMesh(final float[] vertices, final float[] textureCoordinates, final float[] normals, final int[] indices, final RenderMode mode) {
super(mode);
this.vertices = vertices;
this.textureCoords = textureCoordinates;
this.normals = normals;
this.indices = indices;
}
protected ResourceStaticTexturedMesh(final Uri uriFile) {
super(uriFile);
}
/**
* Send the data to the graphic card.
*/
public void flush() {
// request to the manager to be call at the next update ...
this.vao = ResourceVirtualArrayObject.create(this.vertices, this.textureCoords, this.normals, this.indices);
this.vao.flush();
}
}