package com.jpl.games.model; import com.jpl.games.math.Rotations; import com.jpl.games.model3d.Model3D; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import java.util.stream.IntStream; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.LongProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleLongProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.event.EventHandler; import javafx.geometry.Point3D; import javafx.scene.Cursor; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.SubScene; import javafx.scene.input.MouseEvent; import javafx.scene.shape.MeshView; import javafx.scene.transform.Affine; import javafx.scene.transform.Rotate; import javafx.scene.transform.Transform; import javafx.util.Duration; /** * * @author jpereda, April 2014 - @JPeredaDnr */ public class Rubik { private final Group cube=new Group(); private Map<String,MeshView> mapMeshes=new HashMap<>(); private final double dimCube; private final MeshView faceArrow; private final MeshView axisArrow; private final ContentModel content; private final Rotations rot; private final Map<String,Transform> mapTransformsScramble=new HashMap<>(); private final Map<String,Transform> mapTransformsOriginal=new HashMap<>(); private final List<Integer> orderOriginal; private List<Integer> order; private List<Integer> reorder, layer, orderScramble; private List<String> sequence=new ArrayList<>(); private boolean secondRotation=false; private final DoubleProperty rotation=new SimpleDoubleProperty(0d); private final BooleanProperty onRotation=new SimpleBooleanProperty(false); private final BooleanProperty onPreview=new SimpleBooleanProperty(false); private final BooleanProperty onScrambling=new SimpleBooleanProperty(false); private final BooleanProperty onReplaying=new SimpleBooleanProperty(false); private final BooleanProperty hoveredOnClick=new SimpleBooleanProperty(false); private final BooleanProperty solved=new SimpleBooleanProperty(false); private final ObjectProperty<Cursor> cursor = new SimpleObjectProperty<>(Cursor.DEFAULT); private Point3D axis=new Point3D(0,0,0); private final StringProperty previewFace=new SimpleStringProperty(""); private final StringProperty lastRotation=new SimpleStringProperty(""); private final ChangeListener<Number> rotMap; private final IntegerProperty count = new SimpleIntegerProperty(-1); private final LongProperty timestamp = new SimpleLongProperty(0l); private double mouseNewX, mouseNewY, mouseIniX, mouseIniY; private MeshView pickedMesh; private boolean stopEvents=false; private String selFaces="", myFace="", myFaceOld=""; /* r<rMin nothing rMin<=r<rClick preview with selected rotation, on release revert preview rClick<=r preview with selected rotation, and on release click */ private double radius=0d; private static final int MOUSE_OUT=0; private static final int MOUSE_PRESSED=1; private static final int MOUSE_DRAGGED=2; private static final int MOUSE_RELEASED=3; private final IntegerProperty mouse=new SimpleIntegerProperty(MOUSE_OUT); public Rubik(){ /* Import Rubik's Cube model and arrows */ Model3D model=new Model3D(); model.importObj(); mapMeshes=model.getMapMeshes(); faceArrow=model.getFaceArrow(); axisArrow=model.getAxisArrow(); cube.getChildren().setAll(mapMeshes.values()); cube.getChildren().addAll(faceArrow); cube.getChildren().addAll(axisArrow); dimCube=cube.getBoundsInParent().getWidth(); /* Create content subscene, add cube, set camera and lights */ content = new ContentModel(800,600,dimCube); content.setContent(cube); /* Initialize 3D array of indexes and a copy of original/solved position */ rot=new Rotations(); order=rot.getCube(); // System.out.println(""+order.stream().mapToLong(o->mapMeshes.keySet().stream().filter(k->k.contains(o.toString())).count()).sum()); // save original position mapMeshes.forEach((k,v)->mapTransformsOriginal.put(k, v.getTransforms().get(0))); orderOriginal=order.stream().collect(Collectors.toList()); /* Listener to perform an animated face rotation. Note: by prepending the rotations it is not possible to create the animation with a timeline like this: Rotate r=new Rotate(0,axis); v.getTransforms().add(r); Timeline timeline=new Timeline(); timeline.getKeyFrames().add(new KeyFrame(Duration.Seconds(2),new KeyValue(rotation.angle,90))); that takes care of the values: 0<=angle<=90ยบ and transforms the cubies smoothly. So we create the timeline, and listen to how internally it interpolate rotate.angle and perform small rotations between the increments of the angle that the timeline generates: */ rotMap=(ov,angOld,angNew)->{ mapMeshes.forEach((k,v)->{ layer.stream().filter(l->k.contains(l.toString())) .findFirst().ifPresent(l->{ Affine a=new Affine(v.getTransforms().get(0)); a.prepend(new Rotate(angNew.doubleValue()-angOld.doubleValue(),axis)); v.getTransforms().setAll(a); }); }); }; } // called on toolbars buttons click, on mouse released or while scrambling public void rotateFace(final String btRot){ // then bPreview=false, so a full rotation is performed lastRotation.set(""); lastRotation.set(btRot); rotateFace(btRot,false,false); } // called from updateArrow to show a preview with posible cancellation // or from toolbars buttons click, on mouse released or while scrambling to perform rotation private void rotateFace(final String btRot, boolean bPreview, boolean bCancel){ if(onRotation.get()){ return; } onRotation.set(true); System.out.println((bPreview?(bCancel?"Cancelling: ":"Simulating: "):"Rotating: ")+btRot); boolean bFaceArrow= !(btRot.startsWith("X")||btRot.startsWith("Y")||btRot.startsWith("Z")); if(bPreview || onScrambling.get() || onReplaying.get() || secondRotation){ // rotate cube indexes rot.turn(btRot); // get new indexes in terms of blocks numbers from original order reorder=rot.getCube(); // select cubies to rotate: those in reorder different from order. if(!bFaceArrow){ layer=reorder.stream().collect(Collectors.toList()); }else { AtomicInteger index = new AtomicInteger(); layer=order.stream() .filter(o->!Objects.equals(o, reorder.get(index.getAndIncrement()))) .collect(Collectors.toList()); // add central cubie layer.add(0,reorder.get(Utils.getCenter(btRot))); } // set rotation axis axis=Utils.getAxis(btRot); } // define rotation double angIni=(bPreview || onScrambling.get() || onReplaying.get() || secondRotation?0d:5d)*(btRot.endsWith("i")?1d:-1d); double angEnd=(bPreview?5d:90d)*(btRot.endsWith("i")?1d:-1d); rotation.set(angIni); rotation.addListener(rotMap); // create animation Timeline timeline=new Timeline(); timeline.getKeyFrames().add( new KeyFrame(Duration.millis(onScrambling.get() || onReplaying.get()?200:(bPreview?100:600)), e->{ rotation.removeListener(rotMap); secondRotation=false; if(bPreview){ if(bCancel){ previewFace.set(""); onPreview.set(false); } else if(mouse.get()==MOUSE_RELEASED){ // early released, rotate back mouse.set(MOUSE_OUT); onRotation.set(false); updateArrow(btRot,false); } else { previewFace.set(btRot); } } else if(!(onScrambling.get() || onReplaying.get())){ // complete rotation mouse.set(MOUSE_OUT); previewFace.set("V"); if(!hoveredOnClick.get()){ // lost hover event, trigger it to clean up updateArrow(btRot,false); } else { // at the end of rotation, still hovered, if it's clicked again, it's a second rotation secondRotation=true; } hoveredOnClick.set(false); } onRotation.set(false); }, new KeyValue(rotation,angEnd))); timeline.playFromStart(); if(bPreview || onScrambling.get() || onReplaying.get() || secondRotation){ // update order with last list, to start all over again in the next rotation order=reorder.stream().collect(Collectors.toList()); } // count only face rotations not cube rotations if(!bPreview && !onScrambling.get() && bFaceArrow){ count.set(count.get()+1); // check if solved solved.set(Utils.checkSolution(order)); } } // arrow over face or axis to guide user with direction of rotation, complementary with preview // when mouse hover on toolbar buttons or on mouse_dragged start preview, else it is cancelled. public void updateArrow(String face, boolean hover){ boolean bFaceArrow=!(face.startsWith("X")||face.startsWith("Y")||face.startsWith("Z")); MeshView arrow=bFaceArrow?faceArrow:axisArrow; if(hover && onRotation.get()){ return; } arrow.getTransforms().clear(); if(hover){ double d0=arrow.getBoundsInParent().getHeight()/2d; Affine aff=Utils.getAffine(dimCube, d0, bFaceArrow, face); arrow.getTransforms().setAll(aff); arrow.setMaterial(Utils.getMaterial(face)); if(previewFace.get().isEmpty()) { previewFace.set(face); onPreview.set(true); rotateFace(face,true,false); } } else if(previewFace.get().equals(face)){ rotateFace(Utils.reverseRotation(face),true,true); } else if(previewFace.get().equals("V")){ previewFace.set(""); onPreview.set(false); } } // event for mouse picking a cubie and rotate a suitable face public EventHandler<MouseEvent> eventHandler=(MouseEvent event)->{ if (event.getEventType() == MouseEvent.MOUSE_PRESSED || event.getEventType() == MouseEvent.MOUSE_DRAGGED || event.getEventType() == MouseEvent.MOUSE_RELEASED) { //acquire the new Mouse coordinates from the recent event mouseNewX = event.getSceneX(); mouseNewY = -event.getSceneY(); if (event.getEventType() == MouseEvent.MOUSE_PRESSED) { // pick mesh Node picked = event.getPickResult().getIntersectedNode(); if(null != picked && picked instanceof MeshView) { mouse.set(MOUSE_PRESSED); cursor.set(Cursor.CLOSED_HAND); // stop camera events on subscene stopEventHandling(); stopEvents=true; // selected mesh, part of a 6-meshes cubie pickedMesh=(MeshView)picked; // number of block of cubie 46-72 String block=pickedMesh.getId().substring(5,7); // number of cubie 0-26 int indexOf = order.indexOf(new Integer(block)); // select face from cubie and two suitable rotations selFaces=Utils.getPickedRotation(indexOf, pickedMesh); // starting point on the scene (X,Y) mouseIniX=mouseNewX; mouseIniY=mouseNewY; myFace=""; myFaceOld=""; } } else if (event.getEventType() == MouseEvent.MOUSE_DRAGGED) { if(stopEvents && !selFaces.isEmpty()){ mouse.set(MOUSE_DRAGGED); Point3D p=new Point3D(mouseNewX-mouseIniX,mouseNewY-mouseIniY,0); radius=p.magnitude(); if(myFaceOld.isEmpty()){ // when r>rMin it selects one of the two rotations based on x,y movement myFace=Utils.getRightRotation(p,selFaces); if(!myFace.isEmpty() && !onRotation.get()){ // rotation preview updateArrow(myFace, true); myFaceOld=myFace; } if(myFace.isEmpty()){ myFaceOld=""; } } // to cancel preselection, just go back to initial click point if(!myFaceOld.isEmpty() && radius<Utils.radMinimum){ //reset, allowing new face selection myFaceOld=""; // rotation preview cancellation updateArrow(myFace, false); myFace=""; } } } else if (stopEvents && event.getEventType() == MouseEvent.MOUSE_RELEASED) { mouse.set(MOUSE_RELEASED); if(!onRotation.get() && !myFace.isEmpty() && !myFaceOld.isEmpty()){ if(Utils.radClick<radius){ // if hand is moved far away full rotation rotateFace(myFace); } else { // else preview cancellation updateArrow(myFace, false); } } myFace=""; myFaceOld=""; stopEvents=false; resumeEventHandling(); cursor.set(Cursor.DEFAULT); } } }; private String last="V", get="V"; public void doScramble(){ StringBuilder sb=new StringBuilder(); final List<String> movements = Utils.getMovements(); IntStream.range(0, 25).boxed().forEach(i->{ while(last.substring(0, 1).equals(get.substring(0, 1))){ // avoid repeating the same/opposite rotations get=movements.get((int)(Math.floor(Math.random()*movements.size()))); } last=get; if(get.contains("2")){ get=get.substring(0,1); sb.append(get).append(" "); } sb.append(get).append(" "); }); System.out.println("sb: "+sb.toString()); doSequence(sb.toString().trim()); } public void doSequence(String list){ onScrambling.set(true); sequence=Utils.unifyNotation(list); /* This is the way to perform several rotations from a list, waiting till each of them ends properly. A listener is added to onRotation, so only when the last rotation finishes a new rotation is performed. The end of the list is used to stop the listener, by adding a new listener to the index property. Note the size+1, to allow for the last rotation to end. */ IntegerProperty index=new SimpleIntegerProperty(1); ChangeListener<Boolean> lis=(ov,b,b1)->{ if(!b1){ if(index.get()<sequence.size()){ rotateFace(sequence.get(index.get())); } else { // save transforms mapMeshes.forEach((k,v)->mapTransformsScramble.put(k, v.getTransforms().get(0))); orderScramble=reorder.stream().collect(Collectors.toList()); } index.set(index.get()+1); } }; index.addListener((ov,v,v1)->{ if(v1.intValue()==sequence.size()+1){ onScrambling.set(false); onRotation.removeListener(lis); count.set(-1); } }); onRotation.addListener(lis); rotateFace(sequence.get(0)); } public void doReplay(List<Move> moves){ if(moves.isEmpty()){ return; } content.resetCam(); //restore scramble if(mapTransformsScramble.size()>0){ System.out.println("Restoring scramble"); mapMeshes.forEach((k,v)->v.getTransforms().setAll(mapTransformsScramble.get(k))); order=orderScramble.stream().collect(Collectors.toList()); rot.setCube(order); count.set(-1); } else { // restore original doReset(); } onReplaying.set(true); IntegerProperty index=new SimpleIntegerProperty(1); ChangeListener<Boolean> lis=(ov,v,v1)->{ if(!v1 && moves.size()>1){ if(index.get()<moves.size()){ timestamp.set(moves.get(index.get()).getTimestamp()); rotateFace(moves.get(index.get()).getFace()); } index.set(index.get()+1); } }; index.addListener((ov,v,v1)->{ if(v1.intValue()==moves.size()+1){ onReplaying.set(false); onRotation.removeListener(lis); } }); onRotation.addListener(lis); timestamp.set(moves.get(0).getTimestamp()); rotateFace(moves.get(0).getFace()); } public void doReset(){ System.out.println("Reset!"); content.resetCam(); mapMeshes.forEach((k,v)->v.getTransforms().setAll(mapTransformsOriginal.get(k))); order=orderOriginal.stream().collect(Collectors.toList()); rot.setCube(order); count.set(-1); } public SubScene getSubScene(){ return content.getSubScene(); } public BooleanProperty isSolved() { return solved; } public BooleanProperty isOnRotation() { return onRotation; } public BooleanProperty isOnPreview() { return onPreview; } public BooleanProperty isOnScrambling() { return onScrambling; } public BooleanProperty isOnReplaying() { return onReplaying; } public BooleanProperty isHoveredOnClick() { return hoveredOnClick; } public IntegerProperty getCount() { return count; } public LongProperty getTimestamp() { return timestamp; } public StringProperty getPreviewFace() { return previewFace; } public StringProperty getLastRotation() { return lastRotation; } public ObjectProperty<Cursor> getCursor() { return cursor; } public void stopEventHandling(){ content.stopEventHandling(); } public void resumeEventHandling(){ content.resumeEventHandling(); } }