Hi @capdevon
Thank you so much
I do not scale it, but create a new shape on resizing.
Here is the source code for the CollisionShapeEditor class in case you want to take a look. Please feel free to ask if you have a question.
/**
* @author Ali-RS
*/
public class CollisionShapeEditor extends BaseAppState {
static Logger log = LoggerFactory.getLogger(CollisionShapeEditor.class);
private static final String TITLE = "Collision Shapes";
private final Map<Spatial, DebugShape> shapeMap = new HashMap<>();
private final Node modelRoot = new Node("Model Root");
private final Node debugNode = new Node("Debug Shapes");
private final ShapeRemoveListener shapeRemoveListener = new ShapeRemoveListener();
private final List<String> shapeNames = new ArrayList<>();
private Material debugMat;
// Define GUI stuff
private Window window;
private RollupPanel widget;
private CheckboxModel compoundShape;
public CollisionShapeEditor() {
super(TITLE);
setEnabled(false);
}
public void togglePanel() {
setEnabled(!isEnabled());
}
@Override
protected void initialize(Application app) {
URL resource = getClass().getResource("/CollisionShapes");
File file = new File(decodeUrl(resource));
if(file.exists()) {
for (File f : file.listFiles()) {
if( f.getName().matches(".*[-]\\d+[.]sav$") ) {
shapeNames.add(f.getName().replace(".sav", ""));
}
}
}
// Initialize GUI
window = createWindow(TITLE);
getState(MainMenuState.class).registerItem(TITLE, this::togglePanel);
debugMat = GuiGlobals.getInstance().createMaterial(ColorRGBA.Yellow, false).getMaterial();
debugMat.getAdditionalRenderState().setWireframe(true);
}
@Override
protected void cleanup(Application app) {
getState(MainMenuState.class).removeItem(TITLE);
shapeNames.clear();
}
@Override
protected void onEnable() {
getState(WindowManagerState.class).add(window);
Node rootNode = ApplicationGlobals.getInstance().getRootNode();
rootNode.attachChild(modelRoot);
rootNode.attachChild(debugNode);
getState(BlenderCameraState.class).setEnabled(true);
SelectionAppState selectionState = getState(SelectionAppState.class);
selectionState.registerCollisionRoot(debugNode, collisionResult -> {
Spatial hit = collisionResult.getGeometry();
while ((hit = hit.getParent()) != null) {
if(compoundShape.isChecked() && shapeMap.containsKey(hit)) {
Container container = (Container) widget.getContents().getChild("Properties");
container.clearChildren();
container.addChild(shapeMap.get(hit).getProperties());
return hit;
}
}
return null;
});
selectionState.getSelectionModel().addPropertyChangeListener(shapeRemoveListener);
}
@Override
protected void onDisable() {
window.remove();
getState(BlenderCameraState.class).setEnabled(false);
SelectionAppState selectionState = getState(SelectionAppState.class);
selectionState.removeCollisionRoot(debugNode);
selectionState.getSelectionModel().removePropertyChangeListener(shapeRemoveListener);
modelRoot.removeFromParent();
debugNode.removeFromParent();
if(widget != null) closeWidget();
}
protected CollisionShape loadShape(String shapeName) {
CollisionShape shape = null;
URL resource = getClass().getResource("/CollisionShapes/" + shapeName.concat(".sav"));
File f = new File(decodeUrl(resource));
try {
if (!f.exists()) {
log.error("File not exist:" + f);
return null;
}
BinaryImporter imp = BinaryImporter.getInstance();
imp.setAssetManager(getApplication().getAssetManager());
shape = (CollisionShape) imp.load(f);
} catch (IOException ex) {
log.error("Error loading collision shape!", ex);
}
return shape;
}
public void saveShape(CollisionShape shape, String shapeName) {
BinaryExporter ex = BinaryExporter.getInstance();
try {
URL resource = getClass().getResource("/CollisionShapes");
File f = new File(decodeUrl(resource), shapeName.concat(".sav"));
if (!f.exists() && !f.createNewFile()) {
throw new IllegalStateException("File cannot be created:" + f);
}
ex.save(shape, f);
log.info("Exporting collision shape to: {}", f);
} catch (IOException e) {
log.error("Error exporting collision shape!", e);
}
}
private DebugShape createDebugShape(CollisionShape shape) {
if(shape instanceof BoxCollisionShape) {
BoxCollisionShape box = (BoxCollisionShape) shape;
return new BoxDebugShape(box.getHalfExtents(null).multLocal(2));
} else if (shape instanceof SphereCollisionShape) {
SphereCollisionShape sphere = (SphereCollisionShape) shape;
return new SphereDebugShape(sphere.getRadius());
} else if (shape instanceof CylinderCollisionShape) {
CylinderCollisionShape cylinder = (CylinderCollisionShape) shape;
return new CylinderDebugShape(cylinder.maxRadius(), cylinder.getHeight(), cylinder.getAxis());
} else if (shape instanceof CapsuleCollisionShape) {
CapsuleCollisionShape capsule = (CapsuleCollisionShape) shape;
return new CapsuleDebugShape(capsule.getRadius(), capsule.getHeight() + (capsule.getRadius() * 2), capsule.getAxis());
}
throw new IllegalArgumentException("Collision shape type not supported:" + shape.getClass());
}
private Optional<String> getSelectedItem(ListBox<String> listBox) {
Integer selection = listBox.getSelectionModel().getSelection();
return selection != null ? Optional.of(listBox.getModel().get(selection)) : Optional.empty();
}
private Window createWindow(String title) {
Container contents = new Container();
Container listBoxContainer = contents.addChild(new Container());
ListBox<String> categories = listBoxContainer.addChild(new ListBox<>());
categories.setVisibleItems(15);
shapeNames.stream().map(s -> s.split("-")[0]).distinct().forEach(c -> categories.getModel().add(c));
ListBox<String> models = listBoxContainer.addChild(new ListBox<>(), 1);
models.setCellRenderer(new ModelIconRenderer(new Vector2f(100, 100)));
models.setVisibleItems(3);
AlphanumComparator ac = new AlphanumComparator();
categories.addCommands(ListBox.ListAction.Down, source -> getSelectedItem(categories).ifPresent(category -> {
models.getModel().clear();
shapeNames.stream().filter(s -> s.startsWith(category)).forEach(s -> models.getModel().add(s));
Collections.sort(models.getModel(), ac::compare);
}));
Container inputs = contents.addChild(new Container());
TextField nameField = inputs.addChild(new TextField(""));
inputs.addChild(new Button("Create")).addClickCommands(source -> {
if (nameField.getText().trim().isEmpty()) {
return;
}
if (shapeNames.contains(nameField.getText())) {
getState(OptionPanelState.class).show("Error", "Shape with this name already exists.",
new EmptyAction("OK"));
return;
}
saveShape(new EmptyShape(true), nameField.getText());
shapeNames.add(nameField.getText());
String category = nameField.getText().split("-")[0];
if (!categories.getModel().contains(category)) {
categories.getModel().add(category);
}
getSelectedItem(categories).ifPresent(s -> {
if (s.equals(category)) {
models.getModel().add(nameField.getText());
}
});
});
inputs.addChild(new Button("Remove")).addClickCommands(source -> getSelectedItem(models).ifPresent(shapeName -> {
URL resource = getClass().getResource("/CollisionShapes/" + shapeName.concat(".sav"));
File f = new File(decodeUrl(resource));
f.delete();
shapeNames.remove(shapeName);
models.getModel().remove(shapeName);
}));
inputs.addChild(new Button("Edit")).addClickCommands(source -> getSelectedItem(models).ifPresent(shapeName ->
createWidget(shapeName)));
return new JmeWindow(title, contents) {
public void close() { setEnabled(false); }
};
}
private String getModelPath(String shapeName) {
String[] strings = shapeName.split("-");
String category = strings[0];
String modelNumber = strings[1];
return "Models/" + category + "/" + modelNumber + ".j3o";
}
private Spatial loadModel(String shapeName) {
return getApplication().getAssetManager().loadModel(getModelPath(shapeName));
}
private String decodeUrl(URL url) {
return URLDecoder.decode(url.getPath(), StandardCharsets.UTF_8);
}
private RollupPanel createWidget(String shapeName) {
if(widget != null) {
closeWidget();
}
Container contents = new Container();
contents.addChild(new Checkbox("Compound", compoundShape = new DefaultCheckboxModel() {
@Override
public void setChecked(boolean state) {
if(!state && !shapeMap.isEmpty()) {
return;
}
super.setChecked(state);
}
}));
ListBox<String> physicShapes = contents.addChild(new ListBox<>());
physicShapes.setVisibleItems(4);
physicShapes.getChild("selectorArea").removeFromParent();
Container properties = contents.addChild(new Container());;
properties.setName("Properties");
Button save = contents.addChild(new Button("Save"));
contents.addChild(new ActionButton(new CallMethodAction("Close", this, "closeWidget")));
CollisionShape shape = loadShape(shapeName);
if(shape instanceof CompoundCollisionShape) {
Matrix3f tmpRotation = new Matrix3f();
Vector3f tmpOffset = new Vector3f();
for (ChildCollisionShape child : ((CompoundCollisionShape) shape).listChildren()) {
DebugShape debugShape = createDebugShape(child.getShape());
Spatial debugSpatial = debugShape.getDebugSpatial();
// apply scaled offset
child.copyOffset(tmpOffset);
debugSpatial.setLocalTranslation(tmpOffset);
// apply rotation
child.copyRotationMatrix(tmpRotation);
debugSpatial.setLocalRotation(tmpRotation);
debugNode.attachChild(debugSpatial);
shapeMap.put(debugSpatial, debugShape);
}
compoundShape.setChecked(true);
} else if(!(shape instanceof EmptyShape)) {
DebugShape debugShape = createDebugShape(shape);
Spatial debugSpatial = debugShape.getDebugSpatial();
debugNode.attachChild(debugSpatial);
shapeMap.put(debugSpatial, debugShape);
properties.clearChildren();
properties.addChild(debugShape.getProperties());
}
Spatial model = loadModel(shapeName);
modelRoot.attachChild(model);
physicShapes.setCellRenderer(new DefaultCellRenderer<>() {
@Override
public Panel getView(String value, boolean selected, Panel existing) {
if( existing == null ) {
existing = new Button(value);
} else {
((Button) existing).setText(value);
}
return existing;
}
});
physicShapes.getModel().addAll(Arrays.asList("Box", "Sphere", "Cylinder", "Capsule"));
physicShapes.addCommands(ListBox.ListAction.Down, source -> {
Integer selection = physicShapes.getSelectionModel().getSelection();
if(selection != null) {
DebugShape debugShape = null;
switch (physicShapes.getModel().get(selection)) {
case "Box":
debugShape = new BoxDebugShape(Vector3f.UNIT_XYZ.clone());
break;
case "Sphere":
debugShape = new SphereDebugShape(0.5f);
break;
case "Cylinder":
debugShape = new CylinderDebugShape(0.5f, 1, PhysicsSpace.AXIS_Y);
break;
case "Capsule":
debugShape = new CapsuleDebugShape(0.5f, 1, PhysicsSpace.AXIS_Y);
break;
}
Spatial debugSpatial = debugShape.getDebugSpatial();
if(compoundShape.isChecked()) {
getState(SelectionAppState.class).getSelectionModel().setSingleSelection(debugSpatial);
} else {
debugNode.detachAllChildren();
shapeMap.clear();
}
shapeMap.put(debugSpatial, debugShape);
debugNode.attachChild(debugSpatial);
properties.clearChildren();
properties.addChild(debugShape.getProperties());
}
});
save.addClickCommands(source -> {
if(shapeMap.isEmpty()) {
getState(OptionPanelState.class).show("", "Model has no collision shape.", new EmptyAction("OK"));
return;
}
if(compoundShape.isChecked()) {
CompoundCollisionShape ccs = new CompoundCollisionShape();
shapeMap.values().forEach(debugShape ->
ccs.addChildShape(debugShape.createCollisionShape(), debugShape.getDebugSpatial().getLocalTransform()));
saveShape(ccs, shapeName);
} else {
DebugShape debugShape = shapeMap.values().iterator().next();
saveShape(debugShape.createCollisionShape(), shapeName);
}
closeWidget();
});
return widget = getState(WidgetPanelState.class).add(new RollupPanel("Properties", contents, null));
}
private void closeWidget() {
getState(WidgetPanelState.class).remove(widget);
getState(SelectionAppState.class).getSelectionModel().clear();
shapeMap.clear();
modelRoot.detachAllChildren();
debugNode.detachAllChildren();
}
private abstract class DebugShape {
protected final Node debugNode = new Node();
public abstract CollisionShape createCollisionShape();
public abstract PropertyPanel getProperties();
public Node getDebugSpatial() {
return debugNode;
}
protected void updateDebugShape() {
debugNode.detachAllChildren();
Spatial debugShape = DebugShapeFactory.getDebugShape(createCollisionShape());
debugShape.setMaterial(debugMat);
debugNode.attachChild(debugShape);
}
}
protected class BoxDebugShape extends DebugShape {
private final PropertyPanel properties;
private float extentX, extentY, extentZ;
public BoxDebugShape (Vector3f extents) {
this.extentX = extents.x;
this.extentY = extents.y;
this.extentZ = extents.z;
properties = new PropertyPanel(null);
properties.addFloatProperty("X", this, "extentX", 0, 10, 0.05f);
properties.addFloatProperty("Y", this, "extentY", 0, 10, 0.05f);
properties.addFloatProperty("Z", this, "extentZ", 0, 10, 0.05f);
updateDebugShape();
}
public float getExtentX() {
return extentX;
}
public void setExtentX(float extentX) {
this.extentX = extentX;
updateDebugShape();
}
public float getExtentY() {
return extentY;
}
public void setExtentY(float extentY) {
this.extentY = extentY;
updateDebugShape();
}
public float getExtentZ() {
return extentZ;
}
public void setExtentZ(float extentZ) {
this.extentZ = extentZ;
updateDebugShape();
}
@Override
public CollisionShape createCollisionShape() {
return new BoxCollisionShape(getHalfExtents());
}
@Override
public PropertyPanel getProperties() {
return properties;
}
private Vector3f getHalfExtents() {
return new Vector3f(extentX, extentY, extentZ).divideLocal(2);
}
}
protected class SphereDebugShape extends DebugShape {
private final PropertyPanel properties;
private float radius;
public SphereDebugShape (float radius) {
this.radius = radius;
properties = new PropertyPanel(null);
properties.addFloatProperty("Radius", this, "radius", 0, 5, 0.01f);
updateDebugShape();
}
public float getRadius() {
return radius;
}
public void setRadius(float radius) {
this.radius = radius;
updateDebugShape();
}
@Override
public CollisionShape createCollisionShape() {
return new SphereCollisionShape(radius);
}
@Override
public PropertyPanel getProperties() {
return properties;
}
}
protected class CylinderDebugShape extends DebugShape {
private final PropertyPanel properties;
private float radius, height;
private int axis;
public CylinderDebugShape (float radius, float height, int axis) {
this.radius = radius;
this.height = height;
this.axis = axis;
properties = new PropertyPanel(null);
properties.addFloatProperty("Radius", this, "radius", 0, 5, 0.01f);
properties.addFloatProperty("Height", this, "height", 0, 5, 0.01f);
properties.addIntProperty("Axis", this, "axis", 0, 2, 1);
updateDebugShape();
}
public float getRadius() {
return radius;
}
public void setRadius(float radius) {
this.radius = radius;
updateDebugShape();
}
public float getHeight() {
return height;
}
public void setHeight(float height) {
this.height = height;
updateDebugShape();
}
public int getAxis() {
return axis;
}
public void setAxis(int axis) {
this.axis = axis;
updateDebugShape();
}
@Override
public CollisionShape createCollisionShape() {
return new CylinderCollisionShape(radius, height, axis);
}
@Override
public PropertyPanel getProperties() {
return properties;
}
}
protected class CapsuleDebugShape extends CylinderDebugShape {
public CapsuleDebugShape(float radius, float height, int axis) {
super(radius, height, axis);
}
@Override
public CollisionShape createCollisionShape() {
return new CapsuleCollisionShape(getRadius(), Math.max(getHeight() - (getRadius() * 2), 0), getAxis());
}
}
private class ShapeRemoveListener implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if(evt.getPropertyName().equals(SelectionModel.CONTENT_PROPERTY)) {
ObservableList.ElementEvent elementEvent = (ObservableList.ElementEvent) evt;
if(ObservableMap.ChangeType.resolve(elementEvent.getType()) == ObservableMap.ChangeType.REMOVED) {
shapeMap.remove(evt.getOldValue());
}
}
}
}
}