// Copyright 2018 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.arexperiments.justaline.model; import com.arexperiments.justaline.AppSettings; import com.arexperiments.justaline.BiquadFilter; import com.arexperiments.justaline.rendering.LineUtils; import com.google.ar.core.Pose; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.Exclude; import com.google.firebase.database.IgnoreExtraProperties; import com.google.firebase.database.PropertyName; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.vecmath.Vector3f; /** * Created by Kat on 11/6/17. * Single line stroke model for AR */ @IgnoreExtraProperties public class Stroke { private static final String TAG = "Stroke"; @PropertyName("points") private ArrayList<Vector3f> points = new ArrayList<>(); @PropertyName("lineWidth") private float lineWidth; @PropertyName("creator") public String creator = ""; @Exclude private BiquadFilter biquadFilter; @Exclude private BiquadFilter animationFilter; @Exclude public boolean localLine = true; @Exclude public float animatedLength = 0; @Exclude public float totalLength = 0; @Exclude private DatabaseReference firebaseReference; @Exclude public boolean finished = false; public Stroke() { // Default constructor required for calls to DataSnapshot.getValue(Stroke.class) animationFilter = new BiquadFilter(0.025, 1); } // Add point to stroke public void add(Vector3f point) { int s = points.size(); if (s == 0) { // Prepare the biquad filter biquadFilter = new BiquadFilter(AppSettings.getSmoothing(), 3); for (int i = 0; i < AppSettings.getSmoothingCount(); i++) { biquadFilter.update(point); } } // Filter the point point = biquadFilter.update(point); // Check distance, and only add if moved far enough if (s > 0) { Vector3f lastPoint = points.get(s - 1); Vector3f temp = new Vector3f(); temp.sub(point, lastPoint); if (temp.length() < lineWidth / 10) { return; } } // Add the point points.add(point); // Cleanup vertices that are redundant if (s > 3) { float angle = calculateAngle(s - 2); // Remove points that have very low angle change if (angle < 0.05) { points.remove(s - 2); } else { subdivideSection(s - 3, 0.3f, 0); } } // Cleanup beginning, remove points that are close to each other // This makes the end seem straing // if(s < 5 && s > 2){ // float dist = calculateDistance(0, s-1); // if(dist < 0.005) { // for (int i = 0; i < s - 2; i++) { // if (calculateDistance(i, i + 1) < 0.005) { // points.remove(i + 1); // startCap.clear(); // } // } // } // } calculateTotalLength(); } /** * Update called when there is new data from Firebase * * @param data Stroke data to copy from */ public void updateStrokeData(Stroke data) { this.points = data.points; this.lineWidth = data.lineWidth; calculateTotalLength(); } public boolean update() { boolean renderNeedsUpdate = false; if (!localLine) { float before = animatedLength; animatedLength = animationFilter.update(totalLength); if (Math.abs(animatedLength - before) > 0.001) { renderNeedsUpdate = true; } } return renderNeedsUpdate; } public void finishStroke() { finished = true; // Calculate total distance traveled float dist = 0; Vector3f d = new Vector3f(); for (int i = 0; i < points.size() - 1; i++) { d.sub(points.get(i), points.get(i + 1)); dist += d.length(); } // If line is very short, overwrite it if (dist < 0.01) { if (points.size() > 2) { Vector3f p1 = points.get(0); Vector3f p2 = points.get(points.size() - 1); points.clear(); points.add(p1); points.add(p2); } else if (points.size() == 1) { Vector3f v = new Vector3f(points.get(0)); v.y += 0.0005; points.add(v); } } } private float calculateDistance(int index1, int index2) { Vector3f p1 = points.get(index1); Vector3f p2 = points.get(index2); Vector3f n1 = new Vector3f(); n1.sub(p2, p1); return n1.length(); } private float calculateAngle(int index) { Vector3f p1 = points.get(index - 1); Vector3f p2 = points.get(index); Vector3f p3 = points.get(index + 1); Vector3f n1 = new Vector3f(); n1.sub(p2, p1); Vector3f n2 = new Vector3f(); n2.sub(p3, p2); return n1.angle(n2); } public void calculateTotalLength() { totalLength = 0; for (int i = 1; i < points.size(); i++) { Vector3f dist = new Vector3f(points.get(i)); dist.sub(points.get(i - 1)); totalLength += dist.length(); } } private void subdivideSection(int s, float maxAngle, int iteration) { if (iteration == 6) { return; } Vector3f p1 = points.get(s); Vector3f p2 = points.get(s + 1); Vector3f p3 = points.get(s + 2); Vector3f n1 = new Vector3f(); n1.sub(p2, p1); Vector3f n2 = new Vector3f(); n2.sub(p3, p2); float angle = n1.angle(n2); // If angle is too big, add points if (angle > maxAngle) { n1.scale(0.5f); n2.scale(0.5f); n1.add(p1); n2.add(p2); points.add(s + 1, n1); points.add(s + 3, n2); subdivideSection(s + 2, maxAngle, iteration + 1); subdivideSection(s, maxAngle, iteration + 1); } } public void offsetToPose(Pose pose) { for (int i = 0; i < points.size(); i++) { Vector3f p = LineUtils.TransformPointToPose(points.get(i), pose); points.set(i, p); } } public void offsetFromPose(Pose pose) { for (int i = 0; i < points.size(); i++) { Vector3f p = LineUtils.TransformPointFromPose(points.get(i), pose); points.set(i, p); } } public Vector3f get(int index) { return points.get(index); } public int size() { return points.size(); } @SuppressWarnings("unused") public List<Vector3f> getPoints() { return points; } @SuppressWarnings("unused") public float getLineWidth() { return lineWidth; } public void setLineWidth(float lineWidth) { this.lineWidth = lineWidth; } public void setFirebaseValue(StrokeUpdate strokeUpdate, StrokeUpdate previousStrokeUpdate, DatabaseReference.CompletionListener completionListener) { // Stroke copy = new Stroke(); // copy.lineWidth = strokeUpdate.stroke.lineWidth; // int numPointsToSend = strokeUpdate.stroke.points.size(); // copy.points = strokeUpdate.stroke.points.subList(0, strokeUpdate.stroke.points.size()); // // copy.creator = strokeUpdate.stroke.creator; // if points havent been set, or if creator or lineWidth has changed, force a full update if (previousStrokeUpdate == null || previousStrokeUpdate.stroke.points.size() == 0 || !previousStrokeUpdate.stroke.creator.equals(strokeUpdate.stroke.creator) || previousStrokeUpdate.stroke.lineWidth != strokeUpdate.stroke.lineWidth) { firebaseReference.setValue(strokeUpdate.stroke, completionListener); } else { // If only points have updated, calculate the changes since last update, and only upload those points Map<String, Object> pointUpdate = new HashMap<>(); int i = 0; for (Vector3f p : strokeUpdate.stroke.points) { // If point exceeds previous strokes length, add it if (previousStrokeUpdate.stroke.points.size() <= i) { pointUpdate.put(String.valueOf(i), p); } else { // Check if point equals previous point Vector3f prev = previousStrokeUpdate.stroke.points.get(i); if (!p.equals(prev)) { pointUpdate.put(String.valueOf(i), p); } } i++; } firebaseReference.child("points").updateChildren(pointUpdate, completionListener); } } public void removeFirebaseValue() { firebaseReference.removeValue(); } public void setFirebaseReference(DatabaseReference firebaseReference) { this.firebaseReference = firebaseReference; } public boolean hasFirebaseReference() { return firebaseReference != null; } @Exclude public String getFirebaseKey() { return firebaseReference == null ? null : firebaseReference.getKey(); } public Stroke copy() { Stroke copy = new Stroke(); copy.creator = creator; copy.lineWidth = lineWidth; copy.firebaseReference = firebaseReference; copy.points = new ArrayList<>(points); return copy; } }