diff --git a/ugs-core/src/com/willwinder/universalgcodesender/gcode/util/CommandProcessorLoader.java b/ugs-core/src/com/willwinder/universalgcodesender/gcode/util/CommandProcessorLoader.java index f39e8519d0..0ffcb4f383 100644 --- a/ugs-core/src/com/willwinder/universalgcodesender/gcode/util/CommandProcessorLoader.java +++ b/ugs-core/src/com/willwinder/universalgcodesender/gcode/util/CommandProcessorLoader.java @@ -22,69 +22,84 @@ This file is part of Universal Gcode Sender (UGS). import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.willwinder.universalgcodesender.gcode.processors.*; +import com.willwinder.universalgcodesender.gcode.processors.ArcExpander; +import com.willwinder.universalgcodesender.gcode.processors.CommandLengthProcessor; +import com.willwinder.universalgcodesender.gcode.processors.CommandProcessor; +import com.willwinder.universalgcodesender.gcode.processors.CommentProcessor; +import com.willwinder.universalgcodesender.gcode.processors.DecimalProcessor; +import com.willwinder.universalgcodesender.gcode.processors.EmptyLineRemoverProcessor; +import com.willwinder.universalgcodesender.gcode.processors.FeedOverrideProcessor; +import com.willwinder.universalgcodesender.gcode.processors.LineSplitter; +import com.willwinder.universalgcodesender.gcode.processors.M30Processor; +import com.willwinder.universalgcodesender.gcode.processors.PatternRemover; +import com.willwinder.universalgcodesender.gcode.processors.SpindleOnDweller; +import com.willwinder.universalgcodesender.gcode.processors.WhitespaceProcessor; import com.willwinder.universalgcodesender.i18n.Localization; import com.willwinder.universalgcodesender.utils.ControllerSettings.ProcessorConfig; + import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import java.util.logging.Logger; /** - * * @author wwinder */ public class CommandProcessorLoader { + private static final Logger LOGGER = Logger.getLogger(CommandProcessorLoader.class.getSimpleName()); + /** * Add any ICommandProcessors specified in a JSON string. Processors are * initialized using the application settings if they are enabled. - * + *

* JSON Format: * [ - * { - * "name":"ArcExpander", - * "enabled": , - * "optional": , - * "args": {} - * },{ - * "name": "CommandLenghtProcessor", - * "enabled": , - * "optional": , - * "args": {} - * },{ - * "name": "CommentProcessor", - * "enabled": , - * "optional": , - * "args": {} - * },{ - * "name": "DecimalProcessor", - * "enabled": , - * "optional": , - * "args": {} - * },{ - * "name": "FeedOverrideProcessor", - * "enabled": , - * "optional": , - * "args": {} - * },{ - * "name": "M30Processor", - * "enabled": , - * "optional": , - * "args": {} - * },{ - * name: "WhitespaceProcessor", - * "enabled": , - * "optional": , - "args": {} - },{ - name: "SpindleOnDweller", - "enabled": , - * "optional": , - * "args": {} - * } - * ] + * { + * "name":"ArcExpander", + * "enabled": , + * "optional": , + * "args": {} + * },{ + * "name": "CommandLenghtProcessor", + * "enabled": , + * "optional": , + * "args": {} + * },{ + * "name": "CommentProcessor", + * "enabled": , + * "optional": , + * "args": {} + * },{ + * "name": "DecimalProcessor", + * "enabled": , + * "optional": , + * "args": {} + * },{ + * "name": "FeedOverrideProcessor", + * "enabled": , + * "optional": , + * "args": {} + * },{ + * "name": "M30Processor", + * "enabled": , + * "optional": , + * "args": {} + * },{ + * name: "WhitespaceProcessor", + * "enabled": , + * "optional": , + * "args": {} + * },{ + * name: "SpindleOnDweller", + * "enabled": , + * "optional": , + * "args": {} + * } + * ] */ static private List getConfigFrom(String jsonConfig) { List list = new ArrayList<>(); - JsonArray json = new JsonParser().parse(jsonConfig).getAsJsonArray(); + JsonArray json = JsonParser.parseString(jsonConfig).getAsJsonArray(); for (JsonElement entry : json) { JsonObject object = entry.getAsJsonObject(); @@ -115,64 +130,64 @@ static private List getConfigFrom(String jsonConfig) { /** * Add any ICommandProcessors specified in a JSON string. Processors are * configured by properties in the JSON file. - * + *

* JSON Format: * [ { - * "name":"ArcExpander", - * "enabled": , - * "optional": , - * "args": { - * "segmentLengthMM": - * } - * },{ - * "name": "CommandLenghtProcessor", - * "enabled": , - * "optional": , - * "args": { - * "commandLength": - * } - * },{ - * "name": "CommentProcessor", - * "enabled": - * "optional": , - * },{ - * "name": "DecimalProcessor", - * "enabled": , - * "optional": , - * "args": { - * "decimals": - * } - * },{ - * "name": "FeedOverrideProcessor", - * "enabled": , - * "optional": , - * "args": { - * "speed": - * } - * },{ - * "name": "M30Processor", - * "enabled": - * "optional": , - * },{ - * "name": "WhitespaceProcessor", - * "enabled": - * "optional": , - * },{ - * "name": "SpindleOnDweller", - * "enabled": , - * "optional": , - * "args": { - * "duraion": - * } - * },{ - * "name":"LineSplitter", - * "enabled": , - * "optional": , - * "args": { - * "segmentLengthMM": - * } - * } - * ] + * "name":"ArcExpander", + * "enabled": , + * "optional": , + * "args": { + * "segmentLengthMM": + * } + * },{ + * "name": "CommandLenghtProcessor", + * "enabled": , + * "optional": , + * "args": { + * "commandLength": + * } + * },{ + * "name": "CommentProcessor", + * "enabled": + * "optional": , + * },{ + * "name": "DecimalProcessor", + * "enabled": , + * "optional": , + * "args": { + * "decimals": + * } + * },{ + * "name": "FeedOverrideProcessor", + * "enabled": , + * "optional": , + * "args": { + * "speed": + * } + * },{ + * "name": "M30Processor", + * "enabled": + * "optional": , + * },{ + * "name": "WhitespaceProcessor", + * "enabled": + * "optional": , + * },{ + * "name": "SpindleOnDweller", + * "enabled": , + * "optional": , + * "args": { + * "duraion": + * } + * },{ + * "name":"LineSplitter", + * "enabled": , + * "optional": , + * "args": { + * "segmentLengthMM": + * } + * } + * ] */ static public List initializeWithProcessors(String jsonConfig) { return initializeWithProcessors(getConfigFrom(jsonConfig)); @@ -186,7 +201,7 @@ static public List initializeWithProcessors(List initializeWithProcessors(List getProcessor(ProcessorConfig pc) { switch (pc.name) { case "ArcExpander": double length = pc.args.get("segmentLengthMM").getAsDouble(); - return new ArcExpander(true, length); + return Optional.of(new ArcExpander(true, length)); case "CommandLengthProcessor": int commandLength = pc.args.get("commandLength").getAsInt(); - return new CommandLengthProcessor(commandLength); + return Optional.of(new CommandLengthProcessor(commandLength)); case "CommentProcessor": - return new CommentProcessor(); + return Optional.of(new CommentProcessor()); case "DecimalProcessor": int decimals = pc.args.get("decimals").getAsInt(); - return new DecimalProcessor(decimals); + return Optional.of(new DecimalProcessor(decimals)); case "FeedOverrideProcessor": double override = pc.args.get("speedOverridePercent").getAsDouble(); - return new FeedOverrideProcessor(override); + return Optional.of(new FeedOverrideProcessor(override)); case "M30Processor": - return new M30Processor(); + return Optional.of(new M30Processor()); case "PatternRemover": String pattern = pc.args.get("pattern").getAsString(); - return new PatternRemover(pattern); + return Optional.of(new PatternRemover(pattern)); case "WhitespaceProcessor": - return new WhitespaceProcessor(); + return Optional.of(new WhitespaceProcessor()); case "SpindleOnDweller": double duration = pc.args.get("duration").getAsDouble(); - return new SpindleOnDweller(duration); + return Optional.of(new SpindleOnDweller(duration)); case "LineSplitter": - return new LineSplitter(pc.args.get("segmentLengthMM").getAsDouble()); + return Optional.of(new LineSplitter(pc.args.get("segmentLengthMM").getAsDouble())); case "EmptyLineRemoverProcessor": - return new EmptyLineRemoverProcessor(); + return Optional.of(new EmptyLineRemoverProcessor()); default: - throw new IllegalArgumentException("Unknown processor: " + pc.name); + LOGGER.severe("Unknown processor: " + pc.name); + return Optional.empty(); } } } diff --git a/ugs-core/test/com/willwinder/universalgcodesender/gcode/util/CommandProcessorLoaderTest.java b/ugs-core/test/com/willwinder/universalgcodesender/gcode/util/CommandProcessorLoaderTest.java index efa2a20fe0..83cacb5a7c 100644 --- a/ugs-core/test/com/willwinder/universalgcodesender/gcode/util/CommandProcessorLoaderTest.java +++ b/ugs-core/test/com/willwinder/universalgcodesender/gcode/util/CommandProcessorLoaderTest.java @@ -19,9 +19,7 @@ This file is part of Universal Gcode Sender (UGS). package com.willwinder.universalgcodesender.gcode.util; import com.google.gson.JsonArray; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.willwinder.universalgcodesender.gcode.GcodeParser; import com.willwinder.universalgcodesender.gcode.processors.*; import java.util.List; import org.junit.Test; @@ -34,10 +32,7 @@ This file is part of Universal Gcode Sender (UGS). public class CommandProcessorLoaderTest { @Test - public void testInvalidProcessors() throws Exception { - System.out.println("InvalidProcessor"); - GcodeParser gcp = new GcodeParser(); - + public void testInvalidProcessors() { JsonObject args = new JsonObject(); JsonObject object = new JsonObject(); object.addProperty("name", "DoesNotExist"); @@ -46,20 +41,12 @@ public void testInvalidProcessors() throws Exception { JsonArray array = new JsonArray(); array.add(object); - boolean threwException = false; - try { - List result = CommandProcessorLoader.initializeWithProcessors(array.toString()); - } catch (IllegalArgumentException e) { - threwException = true; - } - assertTrue(threwException); + List result = CommandProcessorLoader.initializeWithProcessors(array.toString()); + assertTrue("Invalid processor should be ignored", result.isEmpty()); } @Test - public void testInvalidParametersToValidProcessor() throws Exception { - System.out.println("InvalidProcessor"); - GcodeParser gcp = new GcodeParser(); - + public void testInvalidParametersToValidProcessor() { JsonObject args = new JsonObject(); args.addProperty("segmentLengthMM", "NotANumber"); JsonObject object = new JsonObject(); @@ -83,8 +70,7 @@ public void testInvalidParametersToValidProcessor() throws Exception { */ @Test public void testAllProcessors() throws Exception { - System.out.println("initializeWithProcessors"); - JsonObject args, name, object; + JsonObject args, object; JsonArray array = new JsonArray(); @@ -167,19 +153,4 @@ public void testAllProcessors() throws Exception { assertEquals(SpindleOnDweller.class, processors.get(8).getClass()); assertEquals(LineSplitter.class, processors.get(9).getClass()); } - - private static JsonElement with(String name, Boolean enabled) { - JsonObject object = new JsonObject(); - object.addProperty("name", name); - object.addProperty("enabled", enabled); - return object; - } - - private static JsonElement with(String name, Boolean enabled, Boolean optional) { - JsonObject object = new JsonObject(); - object.addProperty("name", name); - object.addProperty("enabled", enabled); - object.addProperty("optional", optional); - return object; - } } diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/BoundsCollector.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/BoundsCollector.java new file mode 100644 index 0000000000..a4142cc1e5 --- /dev/null +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/BoundsCollector.java @@ -0,0 +1,69 @@ +package com.willwinder.ugs.nbp.designer.entities; + +import com.google.common.collect.Sets; + +import java.awt.geom.Rectangle2D; +import java.util.HashSet; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; + +/** + * A collector for merging the bounds of several entities to one big shape. + * + * @author Joacim Breiler + */ +public class BoundsCollector implements Collector { + + public static final HashSet CHARACTERISTICS = Sets.newHashSet(Characteristics.UNORDERED); + + public static BoundsCollector toBounds() { + return new BoundsCollector(); + } + + private static boolean isIncomplete(Rectangle2D target) { + return Double.isNaN(target.getX()) || Double.isNaN(target.getY()) || Double.isNaN(target.getWidth()) || Double.isNaN(target.getHeight()); + } + + @Override + public Supplier supplier() { + return () -> new Rectangle2D.Double(Double.NaN, Double.NaN, Double.NaN, Double.NaN); + } + + @Override + public BiConsumer accumulator() { + return (target, source) -> combiner().apply(target, source); + } + + @Override + public BinaryOperator combiner() { + return (target, source) -> { + if (isIncomplete(target)) { + target.setRect(source.getX(), source.getY(), source.getWidth(), source.getHeight()); + } else { + target.add(source); + } + + return target; + }; + } + + @Override + public Function finisher() { + return (target) -> { + if (isIncomplete(target)) { + return new Rectangle2D.Double(0, 0, 0, 0); + } else { + return target; + } + }; + } + + @Override + public Set characteristics() { + return CHARACTERISTICS; + } +} diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntityGroup.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntityGroup.java index 563e6a02f3..b557a8b29b 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntityGroup.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/EntityGroup.java @@ -25,7 +25,6 @@ This file is part of Universal Gcode Sender (UGS). import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.AffineTransform; -import java.awt.geom.RectangularShape; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; @@ -43,7 +42,6 @@ public class EntityGroup extends AbstractEntity implements EntityListener { private final List children; private double groupRotation = 0; - private Point2D cachedCenter = new Point2D.Double(0, 0); private Rectangle2D cachedBounds = new Rectangle2D.Double(0, 0, 0, 0); public EntityGroup() { @@ -67,7 +65,8 @@ public void setSize(Size size) { public void rotate(double angle) { try { groupRotation += angle; - getAllChildren().forEach(entity -> entity.rotate(getCenter(), angle)); + Point2D center = getCenter(); + getAllChildren().forEach(entity -> entity.rotate(center, angle)); notifyEvent(new EntityEvent(this, EventType.ROTATED)); } catch (Exception e) { throw new EntityException("Couldn't set the rotation", e); @@ -80,7 +79,7 @@ public void rotate(Point2D center, double angle) { groupRotation += angle; getAllChildren().forEach(entity -> entity.rotate(center, angle)); notifyEvent(new EntityEvent(this, EventType.ROTATED)); - invalidateCenter(); + invalidateBounds(); } catch (Exception e) { throw new EntityException("Couldn't set the rotation", e); } @@ -103,12 +102,9 @@ public Rectangle2D getBounds() { return cachedBounds; } - List allChildren = getAllChildren(); - double maxX = allChildren.stream().map(Entity::getBounds).mapToDouble(RectangularShape::getMaxX).max().orElse(0); - double maxY = allChildren.stream().map(Entity::getBounds).mapToDouble(RectangularShape::getMaxY).max().orElse(0); - double minX = allChildren.stream().map(Entity::getBounds).mapToDouble(RectangularShape::getMinX).min().orElse(0); - double minY = allChildren.stream().map(Entity::getBounds).mapToDouble(RectangularShape::getMinY).min().orElse(0); - cachedBounds = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); + cachedBounds = getAllChildren().stream() + .map(Entity::getBounds) + .collect(BoundsCollector.toBounds()); return cachedBounds; } @@ -131,7 +127,7 @@ public void addChild(Entity entity) { if (!containsChild(entity)) { children.add(entity); entity.addListener(this); - invalidateCenter(); + invalidateBounds(); } } @@ -142,22 +138,17 @@ public void addChild(Entity entity, int index) { children.add(index, entity); entity.addListener(this); - invalidateCenter(); + invalidateBounds(); } - private void invalidateCenter() { - cachedCenter = null; + private void invalidateBounds() { cachedBounds = null; } @Override public Point2D getCenter() { - if (cachedCenter == null) { - Rectangle2D bounds = getBounds(); - cachedCenter = new Point2D.Double(bounds.getCenterX(), bounds.getCenterY()); - } - - return cachedCenter; + Rectangle2D bounds = getBounds(); + return new Point2D.Double(bounds.getCenterX(), bounds.getCenterY()); } public void addAll(List entities) { @@ -167,7 +158,7 @@ public void addAll(List entities) { entity.addListener(this); } }); - invalidateCenter(); + invalidateBounds(); } @Override @@ -175,7 +166,7 @@ public void applyTransform(AffineTransform transform) { if (children != null) { children.forEach(c -> c.applyTransform(transform)); } - invalidateCenter(); + invalidateBounds(); } @Override @@ -183,7 +174,7 @@ public void move(Point2D deltaMovement) { try { applyTransform(AffineTransform.getTranslateInstance(deltaMovement.getX(), deltaMovement.getY())); notifyEvent(new EntityEvent(this, EventType.MOVED)); - invalidateCenter(); + invalidateBounds(); } catch (Exception e) { throw new EntityException("Could not make inverse transform of point", e); } @@ -192,7 +183,7 @@ public void move(Point2D deltaMovement) { @Override public void setTransform(AffineTransform transform) { children.forEach(c -> c.setTransform(transform)); - invalidateCenter(); + invalidateBounds(); } /** @@ -242,7 +233,7 @@ public Optional findParentFor(Entity entity) { public void removeChild(Entity entity) { entity.removeListener(this); children.remove(entity); - invalidateCenter(); + invalidateBounds(); } @Override @@ -255,7 +246,7 @@ public void removeAll() { this.groupRotation = 0; this.children.forEach(entity -> entity.removeListener(this)); this.children.clear(); - invalidateCenter(); + invalidateBounds(); } public List getChildrenAt(Point2D p) { @@ -316,7 +307,7 @@ public void setRotation(double rotation) { } groupRotation += deltaRotation; notifyEvent(new EntityEvent(this, EventType.ROTATED)); - invalidateCenter(); + invalidateBounds(); } public final List getAllChildren() { @@ -347,12 +338,13 @@ public void scale(double sx, double sy) { child.setPosition(new Point2D.Double(originalPosition.getX() + (relativePosition.getX() * sx), originalPosition.getY() + (relativePosition.getY() * sy))); }); notifyEvent(new EntityEvent(this, EventType.RESIZED)); - invalidateCenter(); + invalidateBounds(); } @Override public void onEvent(EntityEvent entityEvent) { notifyEvent(entityEvent); + invalidateBounds(); } @Override diff --git a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Rectangle.java b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Rectangle.java index fdb7a7143a..22a8e2c084 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Rectangle.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/main/java/com/willwinder/ugs/nbp/designer/entities/cuttable/Rectangle.java @@ -21,7 +21,6 @@ This file is part of Universal Gcode Sender (UGS). import com.willwinder.ugs.nbp.designer.entities.Entity; import java.awt.Shape; -import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; /** diff --git a/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/entities/EntityGroupTest.java b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/entities/EntityGroupTest.java index a032cb852b..1cc4111cdf 100644 --- a/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/entities/EntityGroupTest.java +++ b/ugs-platform/ugs-platform-plugin-designer/src/test/java/com/willwinder/ugs/nbp/designer/entities/EntityGroupTest.java @@ -322,4 +322,26 @@ public void findParentForShouldNotReturnParentIfNotFound() { Optional parentFor = entityGroup.findParentFor(point); assertFalse(parentFor.isPresent()); } + + @Test + public void onEventShouldUpdateBounds() { + EntityGroup entityGroup = new EntityGroup(); + Rectangle rectangle1 = new Rectangle(0, 0); + rectangle1.setSize(new Size(10, 10)); + entityGroup.addChild(rectangle1); + + Rectangle rectangle2 = new Rectangle(5, 5); + entityGroup.addChild(rectangle2); + assertEquals(0, entityGroup.getBounds().getX(), 0.1); + assertEquals(0, entityGroup.getBounds().getY(), 0.1); + assertEquals(10, entityGroup.getBounds().getWidth(), 0.1); + assertEquals(10, entityGroup.getBounds().getHeight(), 0.1); + + // Trigger an onEvent which should update the bounds + rectangle2.setSize(new Size(10, 10)); + assertEquals(0, entityGroup.getBounds().getX(), 0.1); + assertEquals(0, entityGroup.getBounds().getY(), 0.1); + assertEquals(15, entityGroup.getBounds().getWidth(), 0.1); + assertEquals(15, entityGroup.getBounds().getHeight(), 0.1); + } }