From 715bc814af428d6d3c7097254934087457aee53f Mon Sep 17 00:00:00 2001 From: Alex Lockwood Date: Mon, 20 Aug 2018 16:37:32 -0700 Subject: [PATCH] pages --> modules --- .../components/canvas/CanvasLayoutMixin.ts | 82 - .../editor/components/canvas/CanvasUtil.ts | 57 - .../components/canvas/PairSubPathHelper.ts | 75 - .../components/canvas/SegmentSplitter.ts | 125 -- .../components/canvas/SelectionHelper.ts | 303 ---- .../editor/components/canvas/ShapeSplitter.ts | 259 --- .../components/canvas/_canvas-theme.scss | 11 - .../components/canvas/canvas.component.html | 29 - .../components/canvas/canvas.component.scss | 42 - .../components/canvas/canvas.component.ts | 161 -- .../canvas/canvascontainer.directive.ts | 24 - .../canvas/canvaslayers.directive.ts | 239 --- .../canvas/canvasoverlay.directive.ts | 1066 ------------ .../canvas/canvaspaper.directive.ts | 43 - .../canvas/canvasruler.directive.ts | 143 -- .../pages/editor/components/canvas/index.ts | 6 - .../components/dialogs/_dialog-theme.scss | 12 - .../dialogs/confirmdialog.component.scss | 10 - .../dialogs/confirmdialog.component.ts | 24 - .../dialogs/demodialog.component.scss | 19 - .../dialogs/demodialog.component.ts | 28 - .../components/dialogs/dialog.service.ts | 29 - .../dialogs/dropfilesdialog.component.scss | 10 - .../dialogs/dropfilesdialog.component.ts | 30 - .../pages/editor/components/dialogs/index.ts | 4 - .../layertimeline/_layerlisttree-theme.scss | 51 - .../layertimeline/_layertimeline-theme.scss | 74 - .../_timelineanimationrow-theme.scss | 27 - .../components/layertimeline/constants.ts | 1 - .../editor/components/layertimeline/index.ts | 4 - .../layerlisttree.component.html | 126 -- .../layerlisttree.component.scss | 193 --- .../layertimeline/layerlisttree.component.ts | 209 --- .../layertimeline.component.html | 252 --- .../layertimeline.component.scss | 242 --- .../layertimeline/layertimeline.component.ts | 1173 -------------- .../layertimelinegrid.directive.ts | 177 -- .../timelineanimationrow.component.html | 41 - .../timelineanimationrow.component.scss | 59 - .../timelineanimationrow.component.ts | 107 -- .../components/playback/_playback-theme.scss | 24 - .../pages/editor/components/playback/index.ts | 1 - .../playback/playback.component.html | 50 - .../playback/playback.component.scss | 54 - .../components/playback/playback.component.ts | 55 - .../pages/editor/components/project/index.ts | 1 - .../components/project/project.service.ts | 33 - .../propertyinput/InspectedProperty.ts | 74 - .../propertyinput/_propertyinput-theme.scss | 66 - .../editor/components/propertyinput/index.ts | 1 - .../propertyinput.component.html | 146 -- .../propertyinput.component.scss | 144 -- .../propertyinput/propertyinput.component.ts | 371 ----- .../editor/components/root/_root-theme.scss | 13 - .../components/root/droptarget.directive.ts | 82 - .../components/root/root.component.html | 63 - .../components/root/root.component.scss | 155 -- .../editor/components/root/root.component.ts | 252 --- .../scrollgroup/scrollgroup.directive.ts | 34 - .../splashscreen/splashscreen.component.html | 10 - .../splashscreen/splashscreen.component.scss | 25 - .../splashscreen/splashscreen.component.ts | 9 - .../splitter/splitter.component.html | 1 - .../splitter/splitter.component.scss | 41 - .../components/splitter/splitter.component.ts | 144 -- .../components/toolbar/_toolbar-theme.scss | 22 - .../components/toolbar/toolbar.component.html | 223 --- .../components/toolbar/toolbar.component.scss | 42 - .../components/toolbar/toolbar.component.ts | 386 ----- .../toolpanel/toolpanel.component.html | 71 - .../toolpanel/toolpanel.component.scss | 17 - .../toolpanel/toolpanel.component.ts | 75 - src/app/pages/editor/editor.module.ts | 133 -- src/app/pages/editor/model/README.md | 155 -- .../pages/editor/model/actionmode/index.ts | 1 - .../pages/editor/model/actionmode/types.ts | 66 - .../model/interpolators/BezierEasing.ts | 113 -- .../model/interpolators/Interpolator.ts | 118 -- .../pages/editor/model/interpolators/index.ts | 1 - src/app/pages/editor/model/layers/Layer.ts | 552 ------- .../pages/editor/model/layers/LayerUtil.ts | 343 ---- src/app/pages/editor/model/layers/index.ts | 14 - .../pages/editor/model/layers/layers.spec.ts | 10 - .../pages/editor/model/paper/CursorType.ts | 27 - src/app/pages/editor/model/paper/ToolMode.ts | 7 - src/app/pages/editor/model/paper/index.ts | 2 - src/app/pages/editor/model/paths/Command.ts | 188 --- .../pages/editor/model/paths/CommandState.ts | 417 ----- src/app/pages/editor/model/paths/Path.spec.ts | 990 ----------- src/app/pages/editor/model/paths/Path.ts | 1441 ----------------- .../editor/model/paths/PathParser.spec.ts | 91 -- .../pages/editor/model/paths/PathParser.ts | 621 ------- src/app/pages/editor/model/paths/PathState.ts | 415 ----- src/app/pages/editor/model/paths/PathUtil.ts | 47 - src/app/pages/editor/model/paths/SubPath.ts | 192 --- .../pages/editor/model/paths/SubPathState.ts | 149 -- src/app/pages/editor/model/paths/SvgChar.ts | 4 - src/app/pages/editor/model/paths/SvgUtil.ts | 179 -- .../paths/calculators/BezierCalculator.ts | 153 -- .../model/paths/calculators/Calculator.ts | 75 - .../model/paths/calculators/LineCalculator.ts | 159 -- .../model/paths/calculators/MoveCalculator.ts | 51 - .../paths/calculators/PointCalculator.ts | 72 - .../editor/model/paths/calculators/index.ts | 1 - src/app/pages/editor/model/paths/index.ts | 8 - .../editor/model/properties/ColorProperty.ts | 44 - .../editor/model/properties/EnumProperty.ts | 28 - .../model/properties/FractionProperty.ts | 22 - .../editor/model/properties/NameProperty.ts | 21 - .../editor/model/properties/NumberProperty.ts | 80 - .../editor/model/properties/PathProperty.ts | 68 - .../pages/editor/model/properties/Property.ts | 111 -- .../editor/model/properties/PropertyMaps.ts | 11 - .../pages/editor/model/properties/index.ts | 8 - .../pages/editor/model/timeline/Animation.ts | 53 - .../editor/model/timeline/AnimationBlock.ts | 150 -- src/app/pages/editor/model/timeline/index.ts | 7 - .../scripts/actionmode/ActionModeUtil.ts | 42 - .../pages/editor/scripts/actionmode/index.ts | 2 - .../scripts/algorithms/AutoAwesome.spec.ts | 57 - .../editor/scripts/algorithms/AutoAwesome.ts | 395 ----- .../scripts/algorithms/NeedlemanWunsch.ts | 77 - .../pages/editor/scripts/algorithms/index.ts | 2 - .../scripts/animator/AnimationRenderer.ts | 95 -- .../pages/editor/scripts/animator/index.ts | 1 - src/app/pages/editor/scripts/bugsnag/index.ts | 15 - .../editor/scripts/common/ColorUtil.spec.ts | 51 - .../pages/editor/scripts/common/ColorUtil.ts | 91 -- .../editor/scripts/common/MathUtil.spec.ts | 16 - .../pages/editor/scripts/common/MathUtil.ts | 70 - .../editor/scripts/common/Matrix.spec.ts | 33 - src/app/pages/editor/scripts/common/Matrix.ts | 156 -- .../pages/editor/scripts/common/ModelUtil.ts | 76 - src/app/pages/editor/scripts/common/Point.ts | 4 - src/app/pages/editor/scripts/common/Rect.ts | 6 - .../editor/scripts/common/TransformUtil.ts | 195 --- src/app/pages/editor/scripts/common/index.ts | 8 - src/app/pages/editor/scripts/demos/index.ts | 12 - .../pages/editor/scripts/dragger/Dragger.ts | 116 -- src/app/pages/editor/scripts/dragger/index.ts | 1 - .../editor/scripts/export/AvdSerializer.ts | 187 --- .../editor/scripts/export/SpriteSerializer.ts | 77 - .../editor/scripts/export/SvgSerializer.ts | 274 ---- .../editor/scripts/export/XmlSerializer.ts | 143 -- src/app/pages/editor/scripts/export/index.ts | 4 - .../editor/scripts/import/SvgLoader.spec.ts | 99 -- .../pages/editor/scripts/import/SvgLoader.ts | 376 ----- .../import/VectorDrawableLoader.spec.ts | 304 ---- .../scripts/import/VectorDrawableLoader.ts | 254 --- src/app/pages/editor/scripts/import/index.ts | 3 - .../editor/scripts/intervals/IntervalTree.ts | 31 - .../pages/editor/scripts/intervals/index.ts | 1 - .../editor/scripts/mixins/DestroyableMixin.ts | 16 - src/app/pages/editor/scripts/mixins/index.ts | 1 - .../editor/scripts/paper/PaperProject.ts | 83 - src/app/pages/editor/scripts/paper/README.md | 131 -- .../scripts/paper/detector/ClickDetector.ts | 85 - .../editor/scripts/paper/detector/Handler.ts | 25 - .../editor/scripts/paper/detector/index.ts | 1 - .../editor/scripts/paper/gesture/Gesture.ts | 19 - .../paper/gesture/create/EllipseGesture.ts | 11 - .../paper/gesture/create/PencilGesture.ts | 74 - .../paper/gesture/create/RectangleGesture.ts | 11 - .../paper/gesture/create/ShapeGesture.ts | 77 - .../scripts/paper/gesture/create/index.ts | 3 - .../edit/BatchSelectSegmentsGesture.ts | 101 -- .../paper/gesture/edit/MouldCurveGesture.ts | 147 -- .../edit/SelectDragDrawSegmentsGesture.ts | 243 --- .../gesture/edit/SelectDragHandleGesture.ts | 96 -- .../edit/ToggleSegmentHandlesGesture.ts | 35 - .../scripts/paper/gesture/edit/index.ts | 5 - .../paper/gesture/hover/HoverGesture.ts | 52 - .../paper/gesture/hover/HoverItemsGesture.ts | 89 - .../hover/HoverSegmentsCurvesGesture.ts | 205 --- .../scripts/paper/gesture/hover/index.ts | 1 - .../editor/scripts/paper/gesture/index.ts | 1 - .../rotate/RotateItemsDragPivotGesture.ts | 63 - .../gesture/rotate/RotateItemsGesture.ts | 125 -- .../scripts/paper/gesture/rotate/index.ts | 2 - .../paper/gesture/scale/ScaleItemsGesture.ts | 253 --- .../scripts/paper/gesture/scale/index.ts | 1 - .../gesture/select/BatchSelectItemsGesture.ts | 74 - .../gesture/select/DeselectItemGesture.ts | 22 - .../paper/gesture/select/EditPathGesture.ts | 30 - .../select/SelectDragCloneItemsGesture.ts | 172 -- .../scripts/paper/gesture/select/index.ts | 4 - .../transform/TransformPathsGesture.ts | 141 -- .../scripts/paper/gesture/transform/index.ts | 1 - src/app/pages/editor/scripts/paper/index.ts | 11 - .../scripts/paper/item/EditPathRaster.ts | 17 - .../editor/scripts/paper/item/HitTests.ts | 92 -- .../editor/scripts/paper/item/PaperLayer.ts | 779 --------- .../paper/item/RotateItemsPivotRaster.ts | 18 - .../paper/item/SelectionBoundsRaster.ts | 49 - .../pages/editor/scripts/paper/item/index.ts | 5 - .../editor/scripts/paper/tool/GestureTool.ts | 277 ---- .../scripts/paper/tool/MasterToolPicker.ts | 64 - .../pages/editor/scripts/paper/tool/Tool.ts | 19 - .../editor/scripts/paper/tool/ZoomPanTool.ts | 108 -- .../pages/editor/scripts/paper/tool/index.ts | 1 - .../editor/scripts/paper/util/PaperUtil.ts | 88 - .../editor/scripts/paper/util/PivotType.ts | 9 - .../pages/editor/scripts/paper/util/index.ts | 4 - .../scripts/paper/util/snap/Constants.ts | 15 - .../scripts/paper/util/snap/SnapBounds.ts | 55 - .../scripts/paper/util/snap/SnapUtil.ts | 335 ---- .../editor/scripts/paper/util/snap/index.ts | 2 - src/app/pages/editor/scripts/svgo/index.ts | 150 -- .../svgo/plugins/convertRoundedRectToPath.ts | 91 -- .../scripts/svgo/plugins/replaceUseElems.ts | 158 -- src/app/pages/editor/services/StoreUtil.ts | 18 - .../editor/services/actionmode.service.ts | 514 ------ .../editor/services/clipboard.service.ts | 133 -- .../editor/services/fileexport.service.ts | 180 -- .../editor/services/fileimport.service.ts | 186 --- src/app/pages/editor/services/index.ts | 10 - .../editor/services/layertimeline.service.ts | 717 -------- .../pages/editor/services/paper.service.ts | 294 ---- .../pages/editor/services/playback.service.ts | 231 --- .../pages/editor/services/shortcut.service.ts | 185 --- .../pages/editor/services/snackbar.service.ts | 16 - .../pages/editor/services/theme.service.ts | 61 - .../pages/editor/store/actionmode/actions.ts | 57 - .../pages/editor/store/actionmode/reducer.ts | 70 - .../editor/store/actionmode/selectors.ts | 152 -- src/app/pages/editor/store/batch/actions.ts | 15 - .../pages/editor/store/batch/metareducer.ts | 11 - .../pages/editor/store/common/selectors.ts | 64 - src/app/pages/editor/store/index.ts | 2 - src/app/pages/editor/store/layers/actions.ts | 47 - src/app/pages/editor/store/layers/reducer.ts | 33 - .../pages/editor/store/layers/selectors.ts | 11 - src/app/pages/editor/store/paper/actions.ts | 146 -- src/app/pages/editor/store/paper/reducer.ts | 81 - src/app/pages/editor/store/paper/selectors.ts | 68 - .../pages/editor/store/playback/actions.ts | 42 - .../pages/editor/store/playback/reducer.ts | 31 - .../pages/editor/store/playback/selectors.ts | 24 - src/app/pages/editor/store/reducer.ts | 58 - src/app/pages/editor/store/reset/actions.ts | 25 - .../pages/editor/store/reset/metareducer.ts | 38 - src/app/pages/editor/store/reset/reducer.ts | 23 - src/app/pages/editor/store/reset/selectors.ts | 5 - src/app/pages/editor/store/selectors.ts | 8 - .../editor/store/storefreeze/metareducer.ts | 18 - src/app/pages/editor/store/theme/actions.ts | 17 - src/app/pages/editor/store/theme/reducer.ts | 28 - src/app/pages/editor/store/theme/selectors.ts | 8 - .../pages/editor/store/timeline/actions.ts | 34 - .../pages/editor/store/timeline/reducer.ts | 29 - .../pages/editor/store/timeline/selectors.ts | 38 - .../editor/store/undoredo/metareducer.ts | 42 - src/app/pages/editor/styles/app.scss | 4 - .../pages/editor/styles/material-icons.scss | 28 - src/app/pages/editor/styles/root.scss | 15 - src/app/pages/editor/styles/theme.scss | 165 -- src/main.ts | 2 +- src/styles.scss | 2 +- src/test/PathUtil.ts | 4 +- 259 files changed, 4 insertions(+), 27657 deletions(-) delete mode 100644 src/app/pages/editor/components/canvas/CanvasLayoutMixin.ts delete mode 100644 src/app/pages/editor/components/canvas/CanvasUtil.ts delete mode 100644 src/app/pages/editor/components/canvas/PairSubPathHelper.ts delete mode 100644 src/app/pages/editor/components/canvas/SegmentSplitter.ts delete mode 100644 src/app/pages/editor/components/canvas/SelectionHelper.ts delete mode 100644 src/app/pages/editor/components/canvas/ShapeSplitter.ts delete mode 100644 src/app/pages/editor/components/canvas/_canvas-theme.scss delete mode 100644 src/app/pages/editor/components/canvas/canvas.component.html delete mode 100644 src/app/pages/editor/components/canvas/canvas.component.scss delete mode 100644 src/app/pages/editor/components/canvas/canvas.component.ts delete mode 100644 src/app/pages/editor/components/canvas/canvascontainer.directive.ts delete mode 100644 src/app/pages/editor/components/canvas/canvaslayers.directive.ts delete mode 100644 src/app/pages/editor/components/canvas/canvasoverlay.directive.ts delete mode 100644 src/app/pages/editor/components/canvas/canvaspaper.directive.ts delete mode 100644 src/app/pages/editor/components/canvas/canvasruler.directive.ts delete mode 100644 src/app/pages/editor/components/canvas/index.ts delete mode 100644 src/app/pages/editor/components/dialogs/_dialog-theme.scss delete mode 100644 src/app/pages/editor/components/dialogs/confirmdialog.component.scss delete mode 100644 src/app/pages/editor/components/dialogs/confirmdialog.component.ts delete mode 100644 src/app/pages/editor/components/dialogs/demodialog.component.scss delete mode 100644 src/app/pages/editor/components/dialogs/demodialog.component.ts delete mode 100644 src/app/pages/editor/components/dialogs/dialog.service.ts delete mode 100644 src/app/pages/editor/components/dialogs/dropfilesdialog.component.scss delete mode 100644 src/app/pages/editor/components/dialogs/dropfilesdialog.component.ts delete mode 100644 src/app/pages/editor/components/dialogs/index.ts delete mode 100644 src/app/pages/editor/components/layertimeline/_layerlisttree-theme.scss delete mode 100644 src/app/pages/editor/components/layertimeline/_layertimeline-theme.scss delete mode 100644 src/app/pages/editor/components/layertimeline/_timelineanimationrow-theme.scss delete mode 100644 src/app/pages/editor/components/layertimeline/constants.ts delete mode 100644 src/app/pages/editor/components/layertimeline/index.ts delete mode 100644 src/app/pages/editor/components/layertimeline/layerlisttree.component.html delete mode 100644 src/app/pages/editor/components/layertimeline/layerlisttree.component.scss delete mode 100644 src/app/pages/editor/components/layertimeline/layerlisttree.component.ts delete mode 100644 src/app/pages/editor/components/layertimeline/layertimeline.component.html delete mode 100644 src/app/pages/editor/components/layertimeline/layertimeline.component.scss delete mode 100644 src/app/pages/editor/components/layertimeline/layertimeline.component.ts delete mode 100644 src/app/pages/editor/components/layertimeline/layertimelinegrid.directive.ts delete mode 100644 src/app/pages/editor/components/layertimeline/timelineanimationrow.component.html delete mode 100644 src/app/pages/editor/components/layertimeline/timelineanimationrow.component.scss delete mode 100644 src/app/pages/editor/components/layertimeline/timelineanimationrow.component.ts delete mode 100644 src/app/pages/editor/components/playback/_playback-theme.scss delete mode 100644 src/app/pages/editor/components/playback/index.ts delete mode 100644 src/app/pages/editor/components/playback/playback.component.html delete mode 100644 src/app/pages/editor/components/playback/playback.component.scss delete mode 100644 src/app/pages/editor/components/playback/playback.component.ts delete mode 100644 src/app/pages/editor/components/project/index.ts delete mode 100644 src/app/pages/editor/components/project/project.service.ts delete mode 100644 src/app/pages/editor/components/propertyinput/InspectedProperty.ts delete mode 100644 src/app/pages/editor/components/propertyinput/_propertyinput-theme.scss delete mode 100644 src/app/pages/editor/components/propertyinput/index.ts delete mode 100644 src/app/pages/editor/components/propertyinput/propertyinput.component.html delete mode 100644 src/app/pages/editor/components/propertyinput/propertyinput.component.scss delete mode 100644 src/app/pages/editor/components/propertyinput/propertyinput.component.ts delete mode 100644 src/app/pages/editor/components/root/_root-theme.scss delete mode 100644 src/app/pages/editor/components/root/droptarget.directive.ts delete mode 100644 src/app/pages/editor/components/root/root.component.html delete mode 100644 src/app/pages/editor/components/root/root.component.scss delete mode 100644 src/app/pages/editor/components/root/root.component.ts delete mode 100644 src/app/pages/editor/components/scrollgroup/scrollgroup.directive.ts delete mode 100644 src/app/pages/editor/components/splashscreen/splashscreen.component.html delete mode 100644 src/app/pages/editor/components/splashscreen/splashscreen.component.scss delete mode 100644 src/app/pages/editor/components/splashscreen/splashscreen.component.ts delete mode 100644 src/app/pages/editor/components/splitter/splitter.component.html delete mode 100644 src/app/pages/editor/components/splitter/splitter.component.scss delete mode 100644 src/app/pages/editor/components/splitter/splitter.component.ts delete mode 100644 src/app/pages/editor/components/toolbar/_toolbar-theme.scss delete mode 100644 src/app/pages/editor/components/toolbar/toolbar.component.html delete mode 100644 src/app/pages/editor/components/toolbar/toolbar.component.scss delete mode 100644 src/app/pages/editor/components/toolbar/toolbar.component.ts delete mode 100644 src/app/pages/editor/components/toolpanel/toolpanel.component.html delete mode 100644 src/app/pages/editor/components/toolpanel/toolpanel.component.scss delete mode 100644 src/app/pages/editor/components/toolpanel/toolpanel.component.ts delete mode 100644 src/app/pages/editor/editor.module.ts delete mode 100644 src/app/pages/editor/model/README.md delete mode 100644 src/app/pages/editor/model/actionmode/index.ts delete mode 100644 src/app/pages/editor/model/actionmode/types.ts delete mode 100644 src/app/pages/editor/model/interpolators/BezierEasing.ts delete mode 100644 src/app/pages/editor/model/interpolators/Interpolator.ts delete mode 100644 src/app/pages/editor/model/interpolators/index.ts delete mode 100644 src/app/pages/editor/model/layers/Layer.ts delete mode 100644 src/app/pages/editor/model/layers/LayerUtil.ts delete mode 100644 src/app/pages/editor/model/layers/index.ts delete mode 100644 src/app/pages/editor/model/layers/layers.spec.ts delete mode 100644 src/app/pages/editor/model/paper/CursorType.ts delete mode 100644 src/app/pages/editor/model/paper/ToolMode.ts delete mode 100644 src/app/pages/editor/model/paper/index.ts delete mode 100644 src/app/pages/editor/model/paths/Command.ts delete mode 100644 src/app/pages/editor/model/paths/CommandState.ts delete mode 100644 src/app/pages/editor/model/paths/Path.spec.ts delete mode 100644 src/app/pages/editor/model/paths/Path.ts delete mode 100644 src/app/pages/editor/model/paths/PathParser.spec.ts delete mode 100644 src/app/pages/editor/model/paths/PathParser.ts delete mode 100644 src/app/pages/editor/model/paths/PathState.ts delete mode 100644 src/app/pages/editor/model/paths/PathUtil.ts delete mode 100644 src/app/pages/editor/model/paths/SubPath.ts delete mode 100644 src/app/pages/editor/model/paths/SubPathState.ts delete mode 100644 src/app/pages/editor/model/paths/SvgChar.ts delete mode 100644 src/app/pages/editor/model/paths/SvgUtil.ts delete mode 100644 src/app/pages/editor/model/paths/calculators/BezierCalculator.ts delete mode 100644 src/app/pages/editor/model/paths/calculators/Calculator.ts delete mode 100644 src/app/pages/editor/model/paths/calculators/LineCalculator.ts delete mode 100644 src/app/pages/editor/model/paths/calculators/MoveCalculator.ts delete mode 100644 src/app/pages/editor/model/paths/calculators/PointCalculator.ts delete mode 100644 src/app/pages/editor/model/paths/calculators/index.ts delete mode 100644 src/app/pages/editor/model/paths/index.ts delete mode 100644 src/app/pages/editor/model/properties/ColorProperty.ts delete mode 100644 src/app/pages/editor/model/properties/EnumProperty.ts delete mode 100644 src/app/pages/editor/model/properties/FractionProperty.ts delete mode 100644 src/app/pages/editor/model/properties/NameProperty.ts delete mode 100644 src/app/pages/editor/model/properties/NumberProperty.ts delete mode 100644 src/app/pages/editor/model/properties/PathProperty.ts delete mode 100644 src/app/pages/editor/model/properties/Property.ts delete mode 100644 src/app/pages/editor/model/properties/PropertyMaps.ts delete mode 100644 src/app/pages/editor/model/properties/index.ts delete mode 100644 src/app/pages/editor/model/timeline/Animation.ts delete mode 100644 src/app/pages/editor/model/timeline/AnimationBlock.ts delete mode 100644 src/app/pages/editor/model/timeline/index.ts delete mode 100644 src/app/pages/editor/scripts/actionmode/ActionModeUtil.ts delete mode 100644 src/app/pages/editor/scripts/actionmode/index.ts delete mode 100644 src/app/pages/editor/scripts/algorithms/AutoAwesome.spec.ts delete mode 100644 src/app/pages/editor/scripts/algorithms/AutoAwesome.ts delete mode 100644 src/app/pages/editor/scripts/algorithms/NeedlemanWunsch.ts delete mode 100644 src/app/pages/editor/scripts/algorithms/index.ts delete mode 100644 src/app/pages/editor/scripts/animator/AnimationRenderer.ts delete mode 100644 src/app/pages/editor/scripts/animator/index.ts delete mode 100644 src/app/pages/editor/scripts/bugsnag/index.ts delete mode 100644 src/app/pages/editor/scripts/common/ColorUtil.spec.ts delete mode 100644 src/app/pages/editor/scripts/common/ColorUtil.ts delete mode 100644 src/app/pages/editor/scripts/common/MathUtil.spec.ts delete mode 100644 src/app/pages/editor/scripts/common/MathUtil.ts delete mode 100644 src/app/pages/editor/scripts/common/Matrix.spec.ts delete mode 100644 src/app/pages/editor/scripts/common/Matrix.ts delete mode 100644 src/app/pages/editor/scripts/common/ModelUtil.ts delete mode 100644 src/app/pages/editor/scripts/common/Point.ts delete mode 100644 src/app/pages/editor/scripts/common/Rect.ts delete mode 100644 src/app/pages/editor/scripts/common/TransformUtil.ts delete mode 100644 src/app/pages/editor/scripts/common/index.ts delete mode 100644 src/app/pages/editor/scripts/demos/index.ts delete mode 100644 src/app/pages/editor/scripts/dragger/Dragger.ts delete mode 100644 src/app/pages/editor/scripts/dragger/index.ts delete mode 100644 src/app/pages/editor/scripts/export/AvdSerializer.ts delete mode 100644 src/app/pages/editor/scripts/export/SpriteSerializer.ts delete mode 100644 src/app/pages/editor/scripts/export/SvgSerializer.ts delete mode 100644 src/app/pages/editor/scripts/export/XmlSerializer.ts delete mode 100644 src/app/pages/editor/scripts/export/index.ts delete mode 100644 src/app/pages/editor/scripts/import/SvgLoader.spec.ts delete mode 100644 src/app/pages/editor/scripts/import/SvgLoader.ts delete mode 100644 src/app/pages/editor/scripts/import/VectorDrawableLoader.spec.ts delete mode 100644 src/app/pages/editor/scripts/import/VectorDrawableLoader.ts delete mode 100644 src/app/pages/editor/scripts/import/index.ts delete mode 100644 src/app/pages/editor/scripts/intervals/IntervalTree.ts delete mode 100644 src/app/pages/editor/scripts/intervals/index.ts delete mode 100644 src/app/pages/editor/scripts/mixins/DestroyableMixin.ts delete mode 100644 src/app/pages/editor/scripts/mixins/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/PaperProject.ts delete mode 100644 src/app/pages/editor/scripts/paper/README.md delete mode 100644 src/app/pages/editor/scripts/paper/detector/ClickDetector.ts delete mode 100644 src/app/pages/editor/scripts/paper/detector/Handler.ts delete mode 100644 src/app/pages/editor/scripts/paper/detector/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/Gesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/create/EllipseGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/create/PencilGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/create/RectangleGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/create/ShapeGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/create/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/edit/BatchSelectSegmentsGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/edit/MouldCurveGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/edit/SelectDragDrawSegmentsGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/edit/SelectDragHandleGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/edit/ToggleSegmentHandlesGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/edit/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/hover/HoverGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/hover/HoverItemsGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/hover/HoverSegmentsCurvesGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/hover/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/rotate/RotateItemsDragPivotGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/rotate/RotateItemsGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/rotate/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/scale/ScaleItemsGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/scale/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/select/BatchSelectItemsGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/select/DeselectItemGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/select/EditPathGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/select/SelectDragCloneItemsGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/select/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/transform/TransformPathsGesture.ts delete mode 100644 src/app/pages/editor/scripts/paper/gesture/transform/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/item/EditPathRaster.ts delete mode 100644 src/app/pages/editor/scripts/paper/item/HitTests.ts delete mode 100644 src/app/pages/editor/scripts/paper/item/PaperLayer.ts delete mode 100644 src/app/pages/editor/scripts/paper/item/RotateItemsPivotRaster.ts delete mode 100644 src/app/pages/editor/scripts/paper/item/SelectionBoundsRaster.ts delete mode 100644 src/app/pages/editor/scripts/paper/item/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/tool/GestureTool.ts delete mode 100644 src/app/pages/editor/scripts/paper/tool/MasterToolPicker.ts delete mode 100644 src/app/pages/editor/scripts/paper/tool/Tool.ts delete mode 100644 src/app/pages/editor/scripts/paper/tool/ZoomPanTool.ts delete mode 100644 src/app/pages/editor/scripts/paper/tool/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/util/PaperUtil.ts delete mode 100644 src/app/pages/editor/scripts/paper/util/PivotType.ts delete mode 100644 src/app/pages/editor/scripts/paper/util/index.ts delete mode 100644 src/app/pages/editor/scripts/paper/util/snap/Constants.ts delete mode 100644 src/app/pages/editor/scripts/paper/util/snap/SnapBounds.ts delete mode 100644 src/app/pages/editor/scripts/paper/util/snap/SnapUtil.ts delete mode 100644 src/app/pages/editor/scripts/paper/util/snap/index.ts delete mode 100644 src/app/pages/editor/scripts/svgo/index.ts delete mode 100644 src/app/pages/editor/scripts/svgo/plugins/convertRoundedRectToPath.ts delete mode 100644 src/app/pages/editor/scripts/svgo/plugins/replaceUseElems.ts delete mode 100644 src/app/pages/editor/services/StoreUtil.ts delete mode 100644 src/app/pages/editor/services/actionmode.service.ts delete mode 100644 src/app/pages/editor/services/clipboard.service.ts delete mode 100644 src/app/pages/editor/services/fileexport.service.ts delete mode 100644 src/app/pages/editor/services/fileimport.service.ts delete mode 100644 src/app/pages/editor/services/index.ts delete mode 100644 src/app/pages/editor/services/layertimeline.service.ts delete mode 100644 src/app/pages/editor/services/paper.service.ts delete mode 100644 src/app/pages/editor/services/playback.service.ts delete mode 100644 src/app/pages/editor/services/shortcut.service.ts delete mode 100644 src/app/pages/editor/services/snackbar.service.ts delete mode 100644 src/app/pages/editor/services/theme.service.ts delete mode 100644 src/app/pages/editor/store/actionmode/actions.ts delete mode 100644 src/app/pages/editor/store/actionmode/reducer.ts delete mode 100644 src/app/pages/editor/store/actionmode/selectors.ts delete mode 100644 src/app/pages/editor/store/batch/actions.ts delete mode 100644 src/app/pages/editor/store/batch/metareducer.ts delete mode 100644 src/app/pages/editor/store/common/selectors.ts delete mode 100644 src/app/pages/editor/store/index.ts delete mode 100644 src/app/pages/editor/store/layers/actions.ts delete mode 100644 src/app/pages/editor/store/layers/reducer.ts delete mode 100644 src/app/pages/editor/store/layers/selectors.ts delete mode 100644 src/app/pages/editor/store/paper/actions.ts delete mode 100644 src/app/pages/editor/store/paper/reducer.ts delete mode 100644 src/app/pages/editor/store/paper/selectors.ts delete mode 100644 src/app/pages/editor/store/playback/actions.ts delete mode 100644 src/app/pages/editor/store/playback/reducer.ts delete mode 100644 src/app/pages/editor/store/playback/selectors.ts delete mode 100644 src/app/pages/editor/store/reducer.ts delete mode 100644 src/app/pages/editor/store/reset/actions.ts delete mode 100644 src/app/pages/editor/store/reset/metareducer.ts delete mode 100644 src/app/pages/editor/store/reset/reducer.ts delete mode 100644 src/app/pages/editor/store/reset/selectors.ts delete mode 100644 src/app/pages/editor/store/selectors.ts delete mode 100644 src/app/pages/editor/store/storefreeze/metareducer.ts delete mode 100644 src/app/pages/editor/store/theme/actions.ts delete mode 100644 src/app/pages/editor/store/theme/reducer.ts delete mode 100644 src/app/pages/editor/store/theme/selectors.ts delete mode 100644 src/app/pages/editor/store/timeline/actions.ts delete mode 100644 src/app/pages/editor/store/timeline/reducer.ts delete mode 100644 src/app/pages/editor/store/timeline/selectors.ts delete mode 100644 src/app/pages/editor/store/undoredo/metareducer.ts delete mode 100644 src/app/pages/editor/styles/app.scss delete mode 100644 src/app/pages/editor/styles/material-icons.scss delete mode 100644 src/app/pages/editor/styles/root.scss delete mode 100644 src/app/pages/editor/styles/theme.scss diff --git a/src/app/pages/editor/components/canvas/CanvasLayoutMixin.ts b/src/app/pages/editor/components/canvas/CanvasLayoutMixin.ts deleted file mode 100644 index 36c6330d..00000000 --- a/src/app/pages/editor/components/canvas/CanvasLayoutMixin.ts +++ /dev/null @@ -1,82 +0,0 @@ -import * as _ from 'lodash'; - -export function CanvasLayoutMixin(Base = class {} as T) { - return class extends Base { - private bounds = { w: 24, h: 24 }; - private viewport = { w: 24, h: 24 }; - private zoom = 1; - private translation = { tx: 0, ty: 0 }; - - /** - * The 'cssScale' represents the number of CSS pixels per SVG viewport pixel. - */ - get cssScale() { - const { w: vWidth, h: vHeight } = this.getViewport(); - const { w: bWidth, h: bHeight } = this.getBounds(); - const vectorAspectRatio = vWidth / vHeight; - const containerAspectRatio = bWidth / bHeight; - if (vectorAspectRatio > containerAspectRatio) { - return bWidth / vWidth; - } else { - return bHeight / vHeight; - } - } - - /** - * The 'attrScale' represents the number of physical pixels per SVG viewport pixel. - */ - get attrScale() { - return this.cssScale * devicePixelRatio; - } - - getBounds() { - return this.bounds; - } - - getViewport() { - return this.viewport; - } - - getZoom() { - return this.zoom; - } - - getTranslation() { - return this.translation; - } - - setDimensions(bounds: Size, viewport: Size) { - if (!_.isEqual(this.bounds, bounds) || !_.isEqual(this.viewport, viewport)) { - this.bounds = bounds; - this.viewport = viewport; - this.onDimensionsChanged(this.bounds, this.viewport); - } - } - - protected onDimensionsChanged(bounds: Size, viewport: Size) {} - - setZoomPan(zoom: number, translation: Readonly<{ tx: number; ty: number }>) { - if (this.zoom !== zoom || !_.isEqual(this.translation, translation)) { - this.zoom = zoom; - this.translation = translation; - this.onZoomPanChanged(zoom, translation); - } - } - - protected onZoomPanChanged(zoom: number, translation: Readonly<{ tx: number; ty: number }>) {} - }; -} - -export interface Size { - readonly w: number; - readonly h: number; -} - -export interface CanvasLayoutMixin { - readonly cssScale: number; - readonly attrScale: number; - getViewport(): Size; - getBounds(): Size; - setDimensions(bounds: Size, viewport: Size): void; - onDimensionsChanged(bounds: Size, viewport: Size): void; -} diff --git a/src/app/pages/editor/components/canvas/CanvasUtil.ts b/src/app/pages/editor/components/canvas/CanvasUtil.ts deleted file mode 100644 index d66e2ff2..00000000 --- a/src/app/pages/editor/components/canvas/CanvasUtil.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Command } from 'app/pages/editor/model/paths'; -import { MathUtil, Matrix, Point } from 'app/pages/editor/scripts/common'; - -type Context = CanvasRenderingContext2D; - -/** - * Executes a series of canvas commands for a given path. - */ -export function executeCommands(ctx: Context, commands: ReadonlyArray, transform: Matrix) { - ctx.save(); - const { a, b, c, d, e, f } = transform; - ctx.transform(a, b, c, d, e, f); - ctx.beginPath(); - - if (commands.length === 1 && commands[0].type !== 'M') { - ctx.moveTo(commands[0].start.x, commands[0].start.y); - } - - let previousEndPoint: Point; - commands.forEach(cmd => { - const start = cmd.start; - const end = cmd.end; - - if (start && !MathUtil.arePointsEqual(start, previousEndPoint)) { - // This is to support the case where the list of commands - // is size fragmented. - ctx.moveTo(start.x, start.y); - } - - if (cmd.type === 'M') { - ctx.moveTo(end.x, end.y); - } else if (cmd.type === 'L') { - ctx.lineTo(end.x, end.y); - } else if (cmd.type === 'Q') { - ctx.quadraticCurveTo(cmd.points[1].x, cmd.points[1].y, cmd.points[2].x, cmd.points[2].y); - } else if (cmd.type === 'C') { - ctx.bezierCurveTo( - cmd.points[1].x, - cmd.points[1].y, - cmd.points[2].x, - cmd.points[2].y, - cmd.points[3].x, - cmd.points[3].y, - ); - } else if (cmd.type === 'Z') { - if (MathUtil.arePointsEqual(start, previousEndPoint)) { - ctx.closePath(); - } else { - // This is to support the case where the list of commands - // is size fragmented. - ctx.lineTo(end.x, end.y); - } - } - previousEndPoint = end; - }); - ctx.restore(); -} diff --git a/src/app/pages/editor/components/canvas/PairSubPathHelper.ts b/src/app/pages/editor/components/canvas/PairSubPathHelper.ts deleted file mode 100644 index 9f948d5c..00000000 --- a/src/app/pages/editor/components/canvas/PairSubPathHelper.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { ActionMode, ActionSource, HoverType } from 'app/pages/editor/model/actionmode'; -import { Point } from 'app/pages/editor/scripts/common'; -import { ActionModeService } from 'app/pages/editor/services'; -import * as _ from 'lodash'; - -import { CanvasOverlayDirective } from './canvasoverlay.directive'; - -// TODO: clean up this class' messy communication w/ the overlay directive - -/** - * Helper class that tracks information during morph subpath mode. - */ -export class PairSubPathHelper { - private readonly actionSource: ActionSource; - private readonly actionModeService: ActionModeService; - - constructor(private readonly component: CanvasOverlayDirective) { - this.actionSource = component.actionSource; - this.actionModeService = component.actionModeService; - } - - onMouseDown(mouseDown: Point, isShiftOrMetaPressed: boolean) { - const hitResult = this.performHitTest(mouseDown); - - if (hitResult.isSegmentHit || hitResult.isShapeHit) { - const hits = hitResult.isShapeHit ? hitResult.shapeHits : hitResult.segmentHits; - const { subIdx } = this.findHitSubPath(hits); - this.actionModeService.pairSubPath(subIdx, this.actionSource); - } else if (!isShiftOrMetaPressed) { - this.actionModeService.setActionMode(ActionMode.Selection); - } - } - - onMouseMove(mouseMove: Point) { - this.checkForHovers(mouseMove); - this.component.draw(); - } - - onMouseUp(mouseUp: Point) { - this.component.draw(); - } - - onMouseLeave(mouseLeave: Point) { - this.component.actionModeService.clearHover(); - this.component.draw(); - } - - private performHitTest(mousePoint: Point) { - return this.component.performHitTest(mousePoint); - } - - private checkForHovers(mousePoint: Point) { - const hitResult = this.performHitTest(mousePoint); - if (!hitResult.isHit) { - this.component.actionModeService.clearHover(); - } else if (hitResult.isSegmentHit || hitResult.isShapeHit) { - const hits = hitResult.isShapeHit ? hitResult.shapeHits : hitResult.segmentHits; - const { subIdx } = this.findHitSubPath(hits); - this.component.actionModeService.setHover({ - type: HoverType.SubPath, - source: this.actionSource, - subIdx, - }); - } - } - - private findHitSubPath(hits: ReadonlyArray<{ subIdx: number }>) { - const infos = hits.map(index => { - const { subIdx } = index; - return { subIdx, subPath: this.component.activePath.getSubPath(subIdx) }; - }); - const lastSplitIndex = _.findLastIndex(infos, info => info.subPath.isSplit()); - return infos[lastSplitIndex < 0 ? infos.length - 1 : lastSplitIndex]; - } -} diff --git a/src/app/pages/editor/components/canvas/SegmentSplitter.ts b/src/app/pages/editor/components/canvas/SegmentSplitter.ts deleted file mode 100644 index d5dfe3c5..00000000 --- a/src/app/pages/editor/components/canvas/SegmentSplitter.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { ActionMode, ActionSource } from 'app/pages/editor/model/actionmode'; -import { ProjectionOntoPath } from 'app/pages/editor/model/paths'; -import { Point } from 'app/pages/editor/scripts/common'; -import { ActionModeService } from 'app/pages/editor/services'; - -import { CanvasOverlayDirective } from './canvasoverlay.directive'; - -interface ProjInfo { - readonly proj: ProjectionOntoPath; - readonly isEndPt: boolean; -} - -// TODO: prefer previous selections over others when performing splits? -// TODO: clean up this class' messy communication w/ the overlay directive - -/** - * Helper class that can be used to split a segment. - */ -export class SegmentSplitter { - private readonly actionSource: ActionSource; - private readonly actionModeService: ActionModeService; - private currProjInfo: ProjInfo; - private lastKnownMouseLocation: Point; - - constructor(private readonly component: CanvasOverlayDirective) { - this.actionSource = component.actionSource; - this.actionModeService = component.actionModeService; - } - - onMouseDown(mouseDown: Point) { - this.lastKnownMouseLocation = mouseDown; - this.currProjInfo = this.findProjInfo(mouseDown); - const activePathLayer = this.component.activePathLayer; - if (this.currProjInfo) { - const { - proj: { subIdx, cmdIdx, projection }, - isEndPt, - } = this.currProjInfo; - const mode = this.component.actionMode; - const pathMutator = activePathLayer.pathData.mutate(); - if (mode === ActionMode.SplitCommands) { - pathMutator.splitCommand(subIdx, cmdIdx, projection.t); - } else if (mode === ActionMode.SplitSubPaths) { - if (!isEndPt) { - pathMutator.splitCommand(subIdx, cmdIdx, projection.t); - } - pathMutator.splitStrokedSubPath(subIdx, cmdIdx); - } - - this.component.actionModeService.clearHover(); - this.actionModeService.setSelections([]); - this.currProjInfo = undefined; - this.actionModeService.updateActivePathBlock(this.actionSource, pathMutator.build()); - this.component.draw(); - return; - } - - this.actionModeService.setActionMode(ActionMode.Selection); - } - - onMouseMove(mouseMove: Point) { - this.lastKnownMouseLocation = mouseMove; - this.currProjInfo = this.findProjInfo(mouseMove); - this.component.draw(); - } - - onMouseUp(mouseUp: Point) { - this.lastKnownMouseLocation = mouseUp; - this.component.draw(); - } - - onMouseLeave(mouseLeave: Point) { - this.lastKnownMouseLocation = mouseLeave; - this.currProjInfo = undefined; - this.component.draw(); - } - - getProjectionOntoPath() { - if (!this.currProjInfo) { - return undefined; - } - return this.currProjInfo.proj; - } - - getLastKnownMouseLocation() { - return this.lastKnownMouseLocation; - } - - private findProjInfo(mousePoint: Point) { - const projInfos: ProjInfo[] = []; - const hitResult = this.component.performHitTest(mousePoint, { withExtraSegmentPadding: true }); - const { isEndPointHit, isSegmentHit, endPointHits, segmentHits } = hitResult; - if (isEndPointHit) { - for (const proj of endPointHits) { - projInfos.push({ proj, isEndPt: true }); - } - } - if (isSegmentHit) { - for (const proj of segmentHits) { - projInfos.push({ proj, isEndPt: false }); - } - } - if (!projInfos.length) { - return undefined; - } - projInfos.sort((p1, p2) => { - const { proj: proj1, isEndPt: isEndPt1 } = p1; - const { proj: proj2, isEndPt: isEndPt2 } = p2; - if (isEndPt1 !== isEndPt2) { - return isEndPt1 ? -1 : 1; - } - if (proj1.projection.d !== proj2.projection.d) { - return proj1.projection.d - proj2.projection.d; - } - if (proj1.subIdx !== proj2.subIdx) { - return proj1.subIdx - proj2.subIdx; - } - if (proj1.cmdIdx !== proj2.cmdIdx) { - return proj1.cmdIdx - proj2.cmdIdx; - } - return 0; - }); - return projInfos[0]; - } -} diff --git a/src/app/pages/editor/components/canvas/SelectionHelper.ts b/src/app/pages/editor/components/canvas/SelectionHelper.ts deleted file mode 100644 index 05b07a6d..00000000 --- a/src/app/pages/editor/components/canvas/SelectionHelper.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { ActionSource, HoverType } from 'app/pages/editor/model/actionmode'; -import { LayerUtil } from 'app/pages/editor/model/layers'; -import { ProjectionOntoPath } from 'app/pages/editor/model/paths'; -import { MathUtil, Point } from 'app/pages/editor/scripts/common'; -import { ActionModeService } from 'app/pages/editor/services'; -import * as _ from 'lodash'; - -import { CanvasOverlayDirective } from './canvasoverlay.directive'; - -// TODO: use the 'Dragger' to drag points to different locations -// TODO: clean up this class' messy communication w/ the overlay directive - -/** - * Helper class that tracks information about a user's mouse gesture, allowing - * for the selection of path points, segments, and shapes. - */ -export class SelectionHelper { - private readonly actionSource: ActionSource; - private readonly actionModeService: ActionModeService; - - // Holds a reference to the currently selected split point, which - // may or may not begin to drag. - private currentDraggableSplitIndex: { subIdx: number; cmdIdx: number } | undefined; - private projectionOntoPath: ProjectionOntoPath; - private isDragTriggered_ = false; - private lastKnownMouseLocation: Point; - private initialMouseDown: Point; - - constructor(private readonly component: CanvasOverlayDirective) { - this.actionSource = component.actionSource; - this.actionModeService = component.actionModeService; - } - - onMouseDown(mouseDown: Point, isShiftOrMetaPressed: boolean) { - this.initialMouseDown = mouseDown; - this.lastKnownMouseLocation = mouseDown; - - const hitResult = this.performHitTest(mouseDown); - if (hitResult.isEndPointHit) { - const { subIdx, cmdIdx, cmd } = this.findHitPoint(hitResult.endPointHits); - if (cmd.isSplitPoint()) { - // Then a click has occurred on top of a split point. - // Don't select the point yet because the user might want - // to drag it to a different location. - this.currentDraggableSplitIndex = { subIdx, cmdIdx }; - } else { - // Then a click has occurred on top of a non-split point. - } - return; - } - - if (this.component.activePathLayer.isFilled() && hitResult.isSegmentHit) { - const { subIdx, cmdIdx, cmd } = this.findHitSegment(hitResult.segmentHits); - if (cmd.isSplitSegment()) { - this.actionModeService.toggleSegmentSelections(this.actionSource, [{ subIdx, cmdIdx }]); - return; - } - } - - if (hitResult.isSegmentHit || hitResult.isShapeHit) { - const hits = hitResult.isShapeHit ? hitResult.shapeHits : hitResult.segmentHits; - const { subIdx } = this.findHitSubPath(hits); - this.actionModeService.toggleSubPathSelection(this.actionSource, subIdx); - } else if (!isShiftOrMetaPressed) { - // If the mouse down event didn't result in a hit, then - // clear any existing selections, but only if the user isn't in - // the middle of selecting multiple points at once. - this.actionModeService.setSelections([]); - } - } - - onMouseMove(mouseMove: Point) { - this.lastKnownMouseLocation = mouseMove; - if (this.currentDraggableSplitIndex) { - const distance = MathUtil.distance(this.initialMouseDown, mouseMove); - if (this.component.dragTriggerTouchSlop < distance) { - this.isDragTriggered_ = true; - } - } - if (this.isDragTriggered_) { - this.projectionOntoPath = this.calculateProjectionOntoPath( - mouseMove, - this.currentDraggableSplitIndex.subIdx, - ); - } else { - this.checkForHovers(mouseMove); - } - this.component.draw(); - } - - onMouseUp(mouseUp: Point, isShiftOrMetaPressed: boolean) { - this.lastKnownMouseLocation = mouseUp; - if (this.isDragTriggered_) { - const projOntoPath = this.projectionOntoPath; - - // TODO: Make this user experience better. There could be other subIdxs that we could use. - const { subIdx: newSubIdx, cmdIdx: newCmdIdx } = projOntoPath; - const { subIdx: oldSubIdx, cmdIdx: oldCmdIdx } = this.currentDraggableSplitIndex; - if (newSubIdx === oldSubIdx) { - const activeLayer = this.component.activePathLayer; - const startingPath = activeLayer.pathData; - let pathMutator = startingPath.mutate(); - - // Note that the order is important here, as it preserves the command indices. - if (newCmdIdx > oldCmdIdx) { - pathMutator.splitCommand(newSubIdx, newCmdIdx, projOntoPath.projection.t); - pathMutator.unsplitCommand(oldSubIdx, oldCmdIdx); - } else if (newCmdIdx < oldCmdIdx) { - pathMutator.unsplitCommand(oldSubIdx, oldCmdIdx); - pathMutator.splitCommand(newSubIdx, newCmdIdx, projOntoPath.projection.t); - } else { - // Unsplitting will cause the projection t value to change, so recalculate the - // projection before the split. - // TODO: improve this API somehow... having to set the active layer here is kind of hacky - activeLayer.pathData = pathMutator.unsplitCommand(oldSubIdx, oldCmdIdx).build(); - const tempProjOntoPath = this.calculateProjectionOntoPath(mouseUp); - if (oldSubIdx === tempProjOntoPath.subIdx) { - pathMutator.splitCommand( - tempProjOntoPath.subIdx, - tempProjOntoPath.cmdIdx, - tempProjOntoPath.projection.t, - ); - } else { - // If for some reason the projection subIdx changes after the unsplit, we have no - // choice but to give up. - // TODO: Make this user experience better. There could be other subIdxs that we could use. - pathMutator = startingPath.mutate(); - } - } - - // Notify the global layer state service about the change and draw. - // Clear any existing selections and/or hovers as well. - this.actionModeService.clearHover(); - this.actionModeService.setSelections([]); - this.reset(); - - this.actionModeService.updateActivePathBlock(this.actionSource, pathMutator.build()); - } - } else if (this.currentDraggableSplitIndex) { - const hitResult = this.performHitTest(mouseUp); - if (!hitResult.isHit) { - this.actionModeService.setSelections([]); - } else if (hitResult.isEndPointHit) { - const { subIdx, cmdIdx } = this.findHitPoint(hitResult.endPointHits); - this.actionModeService.togglePointSelection( - this.actionSource, - subIdx, - cmdIdx, - isShiftOrMetaPressed, - ); - } else if (hitResult.isSegmentHit || hitResult.isShapeHit) { - const hits = hitResult.isShapeHit ? hitResult.shapeHits : hitResult.segmentHits; - const { subIdx } = this.findHitSubPath(hits); - this.actionModeService.toggleSubPathSelection(this.actionSource, subIdx); - } - } - - this.reset(); - this.checkForHovers(mouseUp); - this.component.draw(); - } - - onMouseLeave(mouseLeave: Point) { - this.lastKnownMouseLocation = mouseLeave; - this.reset(); - this.component.actionModeService.clearHover(); - this.component.draw(); - } - - private performHitTest(mousePoint: Point) { - return this.component.performHitTest(mousePoint); - } - - private reset() { - this.initialMouseDown = undefined; - this.projectionOntoPath = undefined; - this.currentDraggableSplitIndex = undefined; - this.isDragTriggered_ = false; - this.lastKnownMouseLocation = undefined; - } - - isDragTriggered() { - return this.isDragTriggered_; - } - - getDraggableSplitIndex() { - return this.currentDraggableSplitIndex; - } - - getProjectionOntoPath() { - return this.projectionOntoPath; - } - - getLastKnownMouseLocation() { - return this.lastKnownMouseLocation; - } - - private checkForHovers(mousePoint: Point) { - if (this.currentDraggableSplitIndex) { - // Don't broadcast new hover events if a point has been selected. - return; - } - const hitResult = this.performHitTest(mousePoint); - if (!hitResult.isHit) { - this.component.actionModeService.clearHover(); - return; - } - if (hitResult.isEndPointHit) { - const { subIdx, cmdIdx } = this.findHitPoint(hitResult.endPointHits); - this.component.actionModeService.setHover({ - type: HoverType.Point, - source: this.actionSource, - subIdx, - cmdIdx, - }); - return; - } - if (hitResult.isSegmentHit) { - if (this.component.activePathLayer.isFilled()) { - const { subIdx, cmdIdx } = this.findHitSegment(hitResult.segmentHits); - if (this.component.activePath.getCommand(subIdx, cmdIdx).isSplitSegment()) { - this.component.actionModeService.setHover({ - type: HoverType.Segment, - source: this.actionSource, - subIdx, - cmdIdx, - }); - return; - } - } else if (this.component.activePathLayer.isStroked()) { - const { subIdx } = this.findHitSegment(hitResult.segmentHits); - this.component.actionModeService.setHover({ - type: HoverType.SubPath, - source: this.actionSource, - subIdx, - }); - return; - } - } - if (hitResult.isShapeHit && this.component.activePathLayer.isFilled()) { - const { subIdx } = this.findHitSubPath(hitResult.shapeHits); - this.component.actionModeService.setHover({ - type: HoverType.SubPath, - source: this.actionSource, - subIdx, - }); - return; - } - } - - private findHitSubPath(hits: ReadonlyArray<{ subIdx: number }>) { - const infos = hits.map(index => { - const { subIdx } = index; - return { subIdx, subPath: this.component.activePath.getSubPath(subIdx) }; - }); - const lastSplitIndex = _.findLastIndex(infos, info => info.subPath.isSplit()); - return infos[lastSplitIndex < 0 ? infos.length - 1 : lastSplitIndex]; - } - - private findHitSegment(hits: ReadonlyArray<{ subIdx: number; cmdIdx: number }>) { - const infos = hits.map(index => { - const { subIdx, cmdIdx } = index; - return { subIdx, cmdIdx, cmd: this.component.activePath.getCommand(subIdx, cmdIdx) }; - }); - const lastSplitIndex = _.findLastIndex(infos, info => info.cmd.isSplitSegment()); - return infos[lastSplitIndex < 0 ? infos.length - 1 : lastSplitIndex]; - } - - private findHitPoint(hits: ReadonlyArray<{ subIdx: number; cmdIdx: number }>) { - const infos = hits.map(index => { - const { subIdx, cmdIdx } = index; - return { subIdx, cmdIdx, cmd: this.component.activePath.getCommand(subIdx, cmdIdx) }; - }); - const lastSplitIndex = _.findLastIndex(infos, info => info.cmd.isSplitPoint()); - return infos[lastSplitIndex < 0 ? infos.length - 1 : lastSplitIndex]; - } - - /** - * Calculates the projection onto the path with the specified path ID. - * The resulting projection is our way of determining the on-curve point - * closest to the specified off-curve mouse point. - */ - private calculateProjectionOntoPath(mousePoint: Point, restrictToSubIdx?: number) { - const canvasToLayerMatrix = LayerUtil.getCanvasTransformForLayer( - this.component.vectorLayer, - this.component.activePathLayer.id, - ).invert(); - if (!canvasToLayerMatrix) { - // Do nothing if matrix is non-invertible. - return undefined; - } - const transformedMousePoint = MathUtil.transformPoint(mousePoint, canvasToLayerMatrix); - const projInfo = this.component.activePath.project(transformedMousePoint, restrictToSubIdx); - if (!projInfo) { - return undefined; - } - return { - subIdx: projInfo.subIdx, - cmdIdx: projInfo.cmdIdx, - projection: projInfo.projection, - }; - } -} diff --git a/src/app/pages/editor/components/canvas/ShapeSplitter.ts b/src/app/pages/editor/components/canvas/ShapeSplitter.ts deleted file mode 100644 index 93b7881e..00000000 --- a/src/app/pages/editor/components/canvas/ShapeSplitter.ts +++ /dev/null @@ -1,259 +0,0 @@ -import { ActionMode } from 'app/pages/editor/model/actionmode'; -import { HitResult, ProjectionOntoPath } from 'app/pages/editor/model/paths'; -import { Point } from 'app/pages/editor/scripts/common'; -import { ActionModeService } from 'app/pages/editor/services'; -import * as _ from 'lodash'; - -import { CanvasOverlayDirective } from './canvasoverlay.directive'; - -interface ProjInfo { - readonly proj: ProjectionOntoPath; - readonly isEndPt: boolean; -} - -// TODO: prefer previous selections over others when performing splits? -// TODO: clean up this class' messy communication w/ the overlay directive - -/** - * Helper class that can be used to split a filled subpath. - */ -export class ShapeSplitter { - private readonly actionModeService: ActionModeService; - private initProjInfos: ProjInfo[] = []; - private finalProjInfos: ProjInfo[] = []; - private hitResult: HitResult; - private lastKnownMouseLocation: Point; - - constructor(private readonly component: CanvasOverlayDirective) { - this.actionModeService = component.actionModeService; - } - - onMouseDown(mouseDown: Point) { - this.initProjInfos = []; - this.finalProjInfos = []; - this.lastKnownMouseLocation = mouseDown; - this.hitResult = this.performHitTest(mouseDown); - const { isEndPointHit, isSegmentHit, endPointHits, segmentHits } = this.hitResult; - if (isEndPointHit || isSegmentHit) { - for (const proj of endPointHits) { - this.initProjInfos.push({ proj, isEndPt: true }); - } - for (const proj of segmentHits) { - this.initProjInfos.push({ proj, isEndPt: false }); - } - this.component.draw(); - return; - } - this.actionModeService.setActionMode(ActionMode.Selection); - } - - onMouseMove(mouseMove: Point) { - this.finalProjInfos = []; - this.lastKnownMouseLocation = mouseMove; - this.hitResult = this.performHitTest(mouseMove); - if (!this.initProjInfos.length) { - this.component.draw(); - return; - } - this.populateFinalProjInfos(mouseMove); - this.component.draw(); - } - - onMouseUp(mouseUp: Point) { - this.finalProjInfos = []; - this.lastKnownMouseLocation = mouseUp; - if (!this.initProjInfos.length) { - this.reset(); - this.component.draw(); - return; - } - this.populateFinalProjInfos(mouseUp); - - const sortProjInfosFn = (projInfos: ProjInfo[]) => { - projInfos.sort((p1, p2) => { - const { proj: proj1, isEndPt: isEndPt1 } = p1; - const { proj: proj2, isEndPt: isEndPt2 } = p2; - if (isEndPt1 !== isEndPt2) { - // Prefer snapping to existing end points first... - return isEndPt1 ? -1 : 1; - } - if (proj1.projection.d !== proj2.projection.d) { - // Then take into account the distance to the new point... - return proj1.projection.d - proj2.projection.d; - } - if (proj1.subIdx !== proj2.subIdx) { - // Then prefer sub paths with higher z-orders... - return proj1.subIdx - proj2.subIdx; - } - if (proj1.cmdIdx !== proj2.cmdIdx) { - // And then finally commands with higher z-orders. - return proj1.cmdIdx - proj2.cmdIdx; - } - return 0; - }); - }; - - sortProjInfosFn(this.initProjInfos); - sortProjInfosFn(this.finalProjInfos); - - let initProjInfo: ProjInfo; - let finalProjInfo: ProjInfo; - for (const p1 of this.initProjInfos) { - for (const p2 of this.finalProjInfos) { - const { - proj: { subIdx: subIdx1, cmdIdx: cmdIdx1 }, - } = p1; - const { - proj: { subIdx: subIdx2, cmdIdx: cmdIdx2 }, - } = p2; - if (subIdx1 === subIdx2) { - if (cmdIdx1 === cmdIdx2) { - continue; - } - initProjInfo = p1; - finalProjInfo = p2; - break; - } - } - if (initProjInfo && finalProjInfo) { - break; - } - } - - if (initProjInfo && finalProjInfo) { - const activeLayer = this.component.activePathLayer; - const pathMutator = activeLayer.pathData.mutate(); - const { - proj: { subIdx: initSubIdx, cmdIdx: initCmdIdx }, - isEndPt: isInitEndPt, - } = initProjInfo; - const { - proj: { subIdx: finalSubIdx, cmdIdx: finalCmdIdx }, - isEndPt: isFinalEndPt, - } = finalProjInfo; - let lastCmdOffset = 0; - if (!isInitEndPt || !isFinalEndPt) { - if (initCmdIdx > finalCmdIdx) { - if (!isInitEndPt) { - pathMutator.splitCommand(initSubIdx, initCmdIdx, initProjInfo.proj.projection.t); - if (!isFinalEndPt) { - lastCmdOffset++; - } - } - if (!isFinalEndPt) { - pathMutator.splitCommand(finalSubIdx, finalCmdIdx, finalProjInfo.proj.projection.t); - if (isInitEndPt) { - lastCmdOffset++; - } - } - } else { - if (!isFinalEndPt) { - pathMutator.splitCommand(finalSubIdx, finalCmdIdx, finalProjInfo.proj.projection.t); - if (!isInitEndPt) { - lastCmdOffset++; - } - } - if (!isInitEndPt) { - pathMutator.splitCommand(initSubIdx, initCmdIdx, initProjInfo.proj.projection.t); - if (isFinalEndPt) { - lastCmdOffset++; - } - } - } - } - - this.component.actionModeService.clearHover(); - this.actionModeService.setSelections([]); - this.reset(); - - // TODO: some bugs with this path: M 0 20 v -16 h 20 v 2 h -12 v 2 h 12 v 2 h -12 Z - // TODO: how should we deal with collinear intersections? (i.e. drawing a line across the same line) - const startingCmdIdx = initCmdIdx > finalCmdIdx ? finalCmdIdx : initCmdIdx; - const endingCmdIdx = - initCmdIdx > finalCmdIdx ? initCmdIdx + lastCmdOffset : finalCmdIdx + lastCmdOffset; - this.actionModeService.updateActivePathBlock( - this.component.actionSource, - pathMutator.splitFilledSubPath(initSubIdx, startingCmdIdx, endingCmdIdx).build(), - ); - } - this.reset(); - this.component.draw(); - } - - onMouseLeave(mouseLeave: Point) { - this.finalProjInfos = []; - this.lastKnownMouseLocation = mouseLeave; - this.hitResult = this.performHitTest(mouseLeave); - if (!this.initProjInfos.length) { - return; - } - this.reset(); - this.component.draw(); - } - - private populateFinalProjInfos(mousePoint: Point) { - const { isEndPointHit, isSegmentHit, endPointHits, segmentHits } = this.hitResult; - if (isEndPointHit || isSegmentHit) { - for (const proj of endPointHits) { - this.finalProjInfos.push({ proj, isEndPt: true }); - } - for (const proj of segmentHits) { - this.finalProjInfos.push({ proj, isEndPt: false }); - } - const allowedSubIdxs = new Set( - this.initProjInfos.map(projInfo => projInfo.proj.subIdx), - ); - _.remove(this.finalProjInfos, projInfo => !allowedSubIdxs.has(projInfo.proj.subIdx)); - } - } - - private performHitTest(mousePoint: Point) { - return this.component.performHitTest(mousePoint, { withExtraSegmentPadding: true }); - } - - private reset() { - this.initProjInfos = []; - this.finalProjInfos = []; - this.hitResult = undefined; - this.lastKnownMouseLocation = undefined; - } - - getCurrentProjectionOntoPath() { - if (!this.hitResult) { - return undefined; - } - const { isEndPointHit, isSegmentHit, endPointHits, segmentHits } = this.hitResult; - if (isEndPointHit) { - return _.last(endPointHits); - } - if (isSegmentHit) { - return _.last(segmentHits); - } - return undefined; - } - - getInitialProjectionOntoPath() { - if (!this.initProjInfos.length) { - return undefined; - } - return this.initProjInfos[0].proj; - } - - getFinalProjectionOntoPath() { - if (!this.finalProjInfos.length) { - return undefined; - } - return this.finalProjInfos[0].proj; - } - - willFinalProjectionOntoPathCreateSplitPoint() { - if (!this.finalProjInfos.length) { - return true; - } - return !this.finalProjInfos[0].isEndPt; - } - - getLastKnownMouseLocation() { - return this.lastKnownMouseLocation; - } -} diff --git a/src/app/pages/editor/components/canvas/_canvas-theme.scss b/src/app/pages/editor/components/canvas/_canvas-theme.scss deleted file mode 100644 index a4ab3e7f..00000000 --- a/src/app/pages/editor/components/canvas/_canvas-theme.scss +++ /dev/null @@ -1,11 +0,0 @@ -@mixin ss-canvas-theme($theme) { - $background: map-get($theme, ss-background); - .app-canvas-container { - .canvas-container { - canvas.rendering-canvas { - // background-color: mat-color($background, base); - background-color: #fff; - } - } - } -} diff --git a/src/app/pages/editor/components/canvas/canvas.component.html b/src/app/pages/editor/components/canvas/canvas.component.html deleted file mode 100644 index e65e3082..00000000 --- a/src/app/pages/editor/components/canvas/canvas.component.html +++ /dev/null @@ -1,29 +0,0 @@ -
- - - - -
- - - - - - -
-
\ No newline at end of file diff --git a/src/app/pages/editor/components/canvas/canvas.component.scss b/src/app/pages/editor/components/canvas/canvas.component.scss deleted file mode 100644 index 2ab0b17a..00000000 --- a/src/app/pages/editor/components/canvas/canvas.component.scss +++ /dev/null @@ -1,42 +0,0 @@ -.app-canvas-container { - position: relative; - .canvas-ruler { - position: absolute; - transition: opacity 0.15s ease, visibility 0s linear 0.15s; - opacity: 0; - visibility: hidden; - } - &:hover .canvas-ruler { - transition: opacity 0.15s ease; - opacity: 1; - visibility: visible; - } - .canvas-container { - position: relative; - canvas.rendering-canvas { - position: absolute; - z-index: 1; - } - canvas.overlay-canvas { - position: absolute; - background-color: transparent; - z-index: 2; - } - canvas.paper-canvas { - position: absolute; - background-color: transparent; - z-index: 3; - } - } - $rulerWidth: 32px; - .orientation-horizontal { - left: -12px; - top: -$rulerWidth; - height: $rulerWidth; - } - .orientation-vertical { - top: -12px; - left: -$rulerWidth; - width: $rulerWidth; - } -} diff --git a/src/app/pages/editor/components/canvas/canvas.component.ts b/src/app/pages/editor/components/canvas/canvas.component.ts deleted file mode 100644 index 6b61967a..00000000 --- a/src/app/pages/editor/components/canvas/canvas.component.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - ElementRef, - HostListener, - Input, - OnInit, - QueryList, - ViewChildren, -} from '@angular/core'; -import { ActionSource } from 'app/pages/editor/model/actionmode'; -import { MathUtil, Matrix } from 'app/pages/editor/scripts/common'; -import { DestroyableMixin } from 'app/pages/editor/scripts/mixins'; -import { ThemeService } from 'app/pages/editor/services'; -import { State, Store } from 'app/pages/editor/store'; -import { isActionMode } from 'app/pages/editor/store/actionmode/selectors'; -import { getVectorLayer } from 'app/pages/editor/store/layers/selectors'; -import { getZoomPanInfo } from 'app/pages/editor/store/paper/selectors'; -import { environment } from 'environments/environment'; -import * as $ from 'jquery'; -import * as _ from 'lodash'; -import { Observable, combineLatest } from 'rxjs'; -import { distinctUntilChanged, map } from 'rxjs/operators'; - -import { CanvasContainerDirective } from './canvascontainer.directive'; -import { CanvasLayersDirective } from './canvaslayers.directive'; -import { CanvasLayoutMixin, Size } from './CanvasLayoutMixin'; -import { CanvasOverlayDirective } from './canvasoverlay.directive'; -import { CanvasPaperDirective } from './canvaspaper.directive'; -import { CanvasRulerDirective } from './canvasruler.directive'; - -// Canvas margin in css pixels. -const CANVAS_MARGIN = 36; - -@Component({ - selector: 'app-canvas', - templateUrl: './canvas.component.html', - styleUrls: ['./canvas.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class CanvasComponent extends CanvasLayoutMixin(DestroyableMixin()) - implements OnInit, AfterViewInit { - readonly IS_BETA = environment.beta; - - @ViewChildren(CanvasContainerDirective) - canvasContainer: QueryList; - @ViewChildren(CanvasLayersDirective) - canvasLayers: QueryList; - @ViewChildren(CanvasOverlayDirective) - canvasOverlay: QueryList; - @ViewChildren(CanvasPaperDirective) - canvasPaper: QueryList; - @ViewChildren(CanvasRulerDirective) - canvasRulers: QueryList; - - @Input() - actionSource: ActionSource; - @Input() - canvasBounds$: Observable; - - private readonly $element: JQuery; - isActionMode$: Observable; - - constructor( - elementRef: ElementRef, - private readonly store: Store, - readonly themeService: ThemeService, - ) { - super(); - this.$element = $(elementRef.nativeElement); - } - - ngOnInit() { - this.isActionMode$ = this.store.select(isActionMode); - } - - ngAfterViewInit() { - const activeViewport$ = this.store.select(getVectorLayer).pipe( - map(vl => ({ w: vl.width, h: vl.height })), - distinctUntilChanged(_.isEqual), - ); - this.registerSubscription( - combineLatest(this.canvasBounds$, activeViewport$).subscribe(([bounds, viewport]) => { - const w = Math.max(1, bounds.w - CANVAS_MARGIN * 2); - const h = Math.max(1, bounds.h - CANVAS_MARGIN * 2); - this.setDimensions({ w, h }, viewport); - }), - ); - this.registerSubscription( - this.store.select(getZoomPanInfo).subscribe(info => { - this.setZoomPan(info.zoom, info.translation); - }), - ); - } - - // @Override - protected onDimensionsChanged(bounds: Size, viewport: Size) { - const directives = [ - ...this.canvasContainer.toArray(), - ...this.canvasLayers.toArray(), - ...this.canvasOverlay.toArray(), - ...this.canvasPaper.toArray(), - ...this.canvasRulers.toArray(), - ]; - directives.forEach(d => d.setDimensions(bounds, viewport)); - } - - // @Override - protected onZoomPanChanged(zoom: number, translation: Readonly<{ tx: number; ty: number }>) { - const directives = [ - ...this.canvasContainer.toArray(), - ...this.canvasLayers.toArray(), - ...this.canvasOverlay.toArray(), - ...this.canvasPaper.toArray(), - ...this.canvasRulers.toArray(), - ]; - directives.forEach(d => d.setZoomPan(zoom, translation)); - } - - @HostListener('mousedown', ['$event']) - onMouseDown(event: MouseEvent) { - this.canvasOverlay.forEach(c => c.onMouseDown(event)); - this.showRuler(event); - } - - @HostListener('mousemove', ['$event']) - onMouseMove(event: MouseEvent) { - this.canvasOverlay.forEach(c => c.onMouseMove(event)); - this.showRuler(event); - } - - @HostListener('mouseup', ['$event']) - onMouseUp(event: MouseEvent) { - this.canvasOverlay.forEach(c => c.onMouseUp(event)); - this.showRuler(event); - } - - @HostListener('mouseleave', ['$event']) - onMouseLeave(event: MouseEvent) { - this.canvasOverlay.forEach(c => c.onMouseLeave(event)); - this.hideRuler(); - } - - private showRuler(event: MouseEvent) { - const canvasOffset = this.$element.offset(); - const zoom = this.getZoom(); - const { tx, ty } = this.getTranslation(); - const point = MathUtil.transformPoint( - { x: event.pageX - canvasOffset.left, y: event.pageY - canvasOffset.top }, - new Matrix(zoom, 0, 0, zoom, tx, ty).invert(), - ); - const x = point.x / Math.max(1, this.cssScale); - const y = point.y / Math.max(1, this.cssScale); - this.canvasRulers.forEach(r => r.showMouse({ x: _.round(x), y: _.round(y) })); - } - - private hideRuler() { - this.canvasRulers.forEach(r => r.hideMouse()); - } -} diff --git a/src/app/pages/editor/components/canvas/canvascontainer.directive.ts b/src/app/pages/editor/components/canvas/canvascontainer.directive.ts deleted file mode 100644 index 42899bc2..00000000 --- a/src/app/pages/editor/components/canvas/canvascontainer.directive.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Directive, ElementRef } from '@angular/core'; -import * as $ from 'jquery'; - -import { CanvasLayoutMixin, Size } from './CanvasLayoutMixin'; - -/** - * Directive that resizes the canvas container when necessary. - */ -@Directive({ selector: '[appCanvasContainer]' }) -export class CanvasContainerDirective extends CanvasLayoutMixin() { - private readonly element: JQuery; - - constructor(elementRef: ElementRef) { - super(); - this.element = $(elementRef.nativeElement); - } - - // @Override - protected onDimensionsChanged(bounds: Size, viewport: Size) { - const { w, h } = viewport; - this.element.attr({ width: w * this.attrScale, height: h * this.attrScale }); - this.element.css({ width: w * this.cssScale, height: h * this.cssScale }); - } -} diff --git a/src/app/pages/editor/components/canvas/canvaslayers.directive.ts b/src/app/pages/editor/components/canvas/canvaslayers.directive.ts deleted file mode 100644 index 7a6236fb..00000000 --- a/src/app/pages/editor/components/canvas/canvaslayers.directive.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core'; -import { ActionSource } from 'app/pages/editor/model/actionmode'; -import { - ClipPathLayer, - Layer, - LayerUtil, - PathLayer, - VectorLayer, -} from 'app/pages/editor/model/layers'; -import { ColorUtil } from 'app/pages/editor/scripts/common'; -import { DestroyableMixin } from 'app/pages/editor/scripts/mixins'; -import { PlaybackService } from 'app/pages/editor/services'; -import { State, Store } from 'app/pages/editor/store'; -import { - getActionModeEndState, - getActionModeStartState, -} from 'app/pages/editor/store/actionmode/selectors'; -import { getHiddenLayerIds, getVectorLayer } from 'app/pages/editor/store/layers/selectors'; -import * as $ from 'jquery'; -import { combineLatest, merge } from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { CanvasLayoutMixin, Size } from './CanvasLayoutMixin'; -import * as CanvasUtil from './CanvasUtil'; - -type Context = CanvasRenderingContext2D; - -/** - * Directive that draws the current vector layer to the canvas. - */ -@Directive({ selector: '[appCanvasLayers]' }) -export class CanvasLayersDirective extends CanvasLayoutMixin(DestroyableMixin()) - implements AfterViewInit { - @Input() - actionSource: ActionSource; - - private readonly $renderingCanvas: JQuery; - private readonly $offscreenCanvas: JQuery; - private vectorLayer: VectorLayer; - private hiddenLayerIds: ReadonlySet = new Set(); - - constructor( - elementRef: ElementRef, - private readonly playbackService: PlaybackService, - private readonly store: Store, - ) { - super(); - this.$renderingCanvas = $(elementRef.nativeElement) as JQuery; - this.$offscreenCanvas = $(document.createElement('canvas')) as JQuery; - } - - ngAfterViewInit() { - if (this.actionSource === ActionSource.Animated) { - // Preview canvas specific setup. - this.registerSubscription( - combineLatest( - // TODO: don't think this is necessary anymore? only need to query playback service now? - merge( - this.playbackService.asObservable().pipe(map(event => event.vl)), - this.store.select(getVectorLayer), - ), - this.store.select(getHiddenLayerIds), - ).subscribe(([vectorLayer, hiddenLayerIds]) => { - this.vectorLayer = vectorLayer; - this.hiddenLayerIds = hiddenLayerIds; - this.draw(); - }), - ); - } else { - // Start & end canvas specific setup. - const actionModeSelector = - this.actionSource === ActionSource.From ? getActionModeStartState : getActionModeEndState; - this.registerSubscription( - this.store.select(actionModeSelector).subscribe(({ vectorLayer, hiddenLayerIds }) => { - this.vectorLayer = vectorLayer; - this.hiddenLayerIds = hiddenLayerIds; - this.draw(); - }), - ); - } - } - - private get renderingCtx() { - return this.$renderingCanvas.get(0).getContext('2d'); - } - - private get offscreenCtx() { - return this.$offscreenCanvas.get(0).getContext('2d'); - } - - // @Override - protected onDimensionsChanged(bounds: Size, viewport: Size) { - const { w, h } = this.getViewport(); - [this.$renderingCanvas, this.$offscreenCanvas].forEach(canvas => { - canvas.attr({ width: w * this.attrScale, height: h * this.attrScale }); - canvas.css({ width: w * this.cssScale, height: h * this.cssScale }); - }); - this.draw(); - } - - private draw() { - if (!this.vectorLayer) { - return; - } - - // Scale the canvas so that everything from this point forward is drawn - // in terms of the SVG's viewport coordinates. - const setupCtxWithViewportCoordsFn = (ctx: Context) => { - ctx.scale(this.attrScale, this.attrScale); - const { w, h } = this.getViewport(); - ctx.clearRect(0, 0, w, h); - }; - - this.renderingCtx.save(); - setupCtxWithViewportCoordsFn(this.renderingCtx); - if (this.vectorLayer.canvasColor) { - this.renderingCtx.fillStyle = ColorUtil.androidToCssRgbaColor(this.vectorLayer.canvasColor); - this.renderingCtx.fillRect(0, 0, this.vectorLayer.width, this.vectorLayer.height); - } - - const currentAlpha = this.vectorLayer ? this.vectorLayer.alpha : 1; - if (currentAlpha < 1) { - this.offscreenCtx.save(); - setupCtxWithViewportCoordsFn(this.offscreenCtx); - } - - // If the canvas is disabled, draw the layer to an offscreen canvas - // so that we can draw it translucently w/o affecting the rest of - // the layer's appearance. - const layerCtx = currentAlpha < 1 ? this.offscreenCtx : this.renderingCtx; - this.drawLayer(this.vectorLayer, this.vectorLayer, layerCtx); - - if (currentAlpha < 1) { - this.renderingCtx.save(); - this.renderingCtx.globalAlpha = currentAlpha; - // Bring the canvas back to its original coordinates before - // drawing the offscreen canvas contents. - this.renderingCtx.scale(1 / this.attrScale, 1 / this.attrScale); - this.renderingCtx.drawImage(this.offscreenCtx.canvas, 0, 0); - this.renderingCtx.restore(); - this.offscreenCtx.restore(); - } - this.renderingCtx.restore(); - } - - private drawLayer(vl: VectorLayer, layer: Layer, ctx: Context) { - if (this.hiddenLayerIds.has(layer.id)) { - return; - } - if (layer instanceof ClipPathLayer) { - this.drawClipPathLayer(vl, layer, ctx); - } else if (layer instanceof PathLayer) { - this.drawPathLayer(vl, layer, ctx); - } else { - ctx.save(); - layer.children.forEach(child => this.drawLayer(vl, child, ctx)); - ctx.restore(); - } - } - - private drawClipPathLayer(vl: VectorLayer, layer: ClipPathLayer, ctx: Context) { - if (!layer.pathData || !layer.pathData.getCommands().length) { - return; - } - const flattenedTransform = LayerUtil.getCanvasTransformForLayer(vl, layer.id); - CanvasUtil.executeCommands(ctx, layer.pathData.getCommands(), flattenedTransform); - ctx.clip(); - } - - private drawPathLayer(vl: VectorLayer, layer: PathLayer, ctx: Context) { - if (!layer.pathData || !layer.pathData.getCommands().length) { - return; - } - const layerToCanvasMatrix = LayerUtil.getCanvasTransformForLayer(vl, layer.id); - const canvasToLayerMatrix = layerToCanvasMatrix.invert(); - if (!canvasToLayerMatrix) { - // Do nothing if matrix is non-invertible. - return; - } - - ctx.save(); - CanvasUtil.executeCommands(ctx, layer.pathData.getCommands(), layerToCanvasMatrix); - - const strokeWidthMultiplier = canvasToLayerMatrix.getScaleFactor(); - ctx.strokeStyle = ColorUtil.androidToCssRgbaColor(layer.strokeColor, layer.strokeAlpha); - ctx.lineWidth = layer.strokeWidth * strokeWidthMultiplier; - ctx.fillStyle = ColorUtil.androidToCssRgbaColor(layer.fillColor, layer.fillAlpha); - ctx.lineCap = layer.strokeLinecap; - ctx.lineJoin = layer.strokeLinejoin; - ctx.miterLimit = layer.strokeMiterLimit; - - if (layer.trimPathStart !== 0 || layer.trimPathEnd !== 1 || layer.trimPathOffset !== 0) { - const { a, d } = canvasToLayerMatrix; - // Note that we only return the length of the first sub path due to - // https://code.google.com/p/android/issues/detail?id=172547 - let pathLength: number; - if (Math.abs(a) !== 1 || Math.abs(d) !== 1) { - // Then recompute the scaled path length. - pathLength = layer.pathData - .mutate() - .transform(canvasToLayerMatrix) - .build() - .getSubPathLength(0); - } else { - pathLength = layer.pathData.getSubPathLength(0); - } - - const strokeDashArray = LayerUtil.toStrokeDashArray( - layer.trimPathStart, - layer.trimPathEnd, - layer.trimPathOffset, - pathLength, - 0.001, - ); - const strokeDashOffset = LayerUtil.toStrokeDashOffset( - layer.trimPathStart, - layer.trimPathEnd, - layer.trimPathOffset, - pathLength, - ); - ctx.setLineDash(strokeDashArray); - ctx.lineDashOffset = strokeDashOffset; - } else { - ctx.setLineDash([]); - } - if (layer.isStroked() && layer.strokeWidth && layer.trimPathStart !== layer.trimPathEnd) { - ctx.stroke(); - } - if (layer.isFilled()) { - if (layer.fillType === 'evenOdd') { - // Unlike VectorDrawables, SVGs spell 'evenodd' with a lowercase 'o'. - ctx.fill('evenodd'); - } else { - ctx.fill(); - } - } - ctx.restore(); - } -} diff --git a/src/app/pages/editor/components/canvas/canvasoverlay.directive.ts b/src/app/pages/editor/components/canvas/canvasoverlay.directive.ts deleted file mode 100644 index bd5215b4..00000000 --- a/src/app/pages/editor/components/canvas/canvasoverlay.directive.ts +++ /dev/null @@ -1,1066 +0,0 @@ -import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core'; -import { - ActionMode, - ActionSource, - Hover, - HoverType, - Selection, - SelectionType, -} from 'app/pages/editor/model/actionmode'; -import { - ClipPathLayer, - GroupLayer, - Layer, - LayerUtil, - MorphableLayer, - PathLayer, - VectorLayer, -} from 'app/pages/editor/model/layers'; -import { Command, HitResult, Path, SubPath } from 'app/pages/editor/model/paths'; -import { MathUtil, Matrix, Point } from 'app/pages/editor/scripts/common'; -import { DestroyableMixin } from 'app/pages/editor/scripts/mixins'; -import { - ActionModeService, - LayerTimelineService, - PlaybackService, - ShortcutService, -} from 'app/pages/editor/services'; -import { State, Store } from 'app/pages/editor/store'; -import { - getActionMode, - getActionModeEndState, - getActionModeHover, - getActionModeStartState, -} from 'app/pages/editor/store/actionmode/selectors'; -import { getCanvasOverlayState } from 'app/pages/editor/store/common/selectors'; -import { getVectorLayer } from 'app/pages/editor/store/layers/selectors'; -import * as $ from 'jquery'; -import * as _ from 'lodash'; -import { combineLatest , merge } from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { CanvasLayoutMixin } from './CanvasLayoutMixin'; -import * as CanvasUtil from './CanvasUtil'; -import { PairSubPathHelper } from './PairSubPathHelper'; -import { SegmentSplitter } from './SegmentSplitter'; -import { SelectionHelper } from './SelectionHelper'; -import { ShapeSplitter } from './ShapeSplitter'; - -// The line width of a highlight in css pixels. -const HIGHLIGHT_LINE_WIDTH = 6; -// The line dash of a highlight in css pixels. -const HIGHLIGHT_LINE_DASH = 12; -// The minimum distance between a point and a path that causes a snap. -const MIN_SNAP_THRESHOLD = 12; -// The distance of a mouse gesture that triggers a drag, in css pixels. -const DRAG_TRIGGER_TOUCH_SLOP = 6; -// The radius of a medium point in css pixels. -const MEDIUM_POINT_RADIUS = 8; -// The radius of a small point in css pixels. -const SMALL_POINT_RADIUS = MEDIUM_POINT_RADIUS / 1.7; - -const SPLIT_POINT_RADIUS_FACTOR = 0.8; -const SELECTED_POINT_RADIUS_FACTOR = 1.25; -const POINT_BORDER_FACTOR = 1.075; - -const NORMAL_POINT_COLOR = '#2962FF'; // Blue A400 -const SPLIT_POINT_COLOR = '#E65100'; // Orange 900 -const HIGHLIGHT_COLOR = '#448AFF'; -const POINT_BORDER_COLOR = '#000'; -const POINT_TEXT_COLOR = '#fff'; -const ERROR_COLOR = '#F44336'; - -// TODO: make shape shifter mode work with clip paths -// TODO: make segment splitter work with trim paths -// TODO: make trim paths work with shifts/reversals -// TODO: make cursor 'drag' in selection mode when dragging points -// TODO: need to avoid cases where the pathData could be undefined -// (i.e. this could happen if the user enters action mode w/o setting a path string on the layer) - -type Context = CanvasRenderingContext2D; - -/** - * A directive that draws overlay selections and other content on top - * of the currently active vector layer. - */ -@Directive({ selector: '[appCanvasOverlay]' }) -export class CanvasOverlayDirective extends CanvasLayoutMixin(DestroyableMixin()) - implements AfterViewInit { - @Input() actionSource: ActionSource; - - private readonly $canvas: JQuery; - vectorLayer: VectorLayer; - // Normal mode variables. - private hiddenLayerIds: ReadonlySet = new Set(); - private selectedLayerIds: ReadonlySet = new Set(); - // Shape Shifter mode variables. - private blockLayerId: string; - actionMode: ActionMode; - private actionHover: Hover; - actionSelections: ReadonlyArray; - private currentHoverPreviewPath: Path | undefined; - pairedSubPaths: Set; - unpairedSubPath: { source: ActionSource; subIdx: number }; - private isActionMode: boolean; - private selectedBlockLayerIds: ReadonlySet = new Set(); - private subIdxWithError: number; - - private selectionHelper: SelectionHelper | undefined; - private pairSubPathHelper: PairSubPathHelper | undefined; - private segmentSplitter: SegmentSplitter | undefined; - private shapeSplitter: ShapeSplitter | undefined; - - constructor( - elementRef: ElementRef, - readonly store: Store, - readonly actionModeService: ActionModeService, - private readonly playbackService: PlaybackService, - private readonly layerTimelineService: LayerTimelineService, - ) { - super(); - this.$canvas = $(elementRef.nativeElement) as JQuery; - } - - ngAfterViewInit() { - if (this.actionSource === ActionSource.Animated) { - // Animated canvas specific setup. - this.registerSubscription( - combineLatest( - // TODO: don't think this is necessary anymore? only need to query playback service now? - merge( - this.playbackService.asObservable().pipe(map(event => event.vl)), - this.store.select(getVectorLayer), - ), - this.store.select(getCanvasOverlayState), - ).subscribe( - ([ - vectorLayer, - { hiddenLayerIds, selectedLayerIds, isActionMode, selectedBlockLayerIds }, - ]) => { - this.vectorLayer = vectorLayer; - this.hiddenLayerIds = hiddenLayerIds; - this.selectedLayerIds = selectedLayerIds; - this.isActionMode = isActionMode; - this.selectedBlockLayerIds = selectedBlockLayerIds; - this.draw(); - }, - ), - ); - } else { - // From & to canvas specific setup. - const shapeShifterSelector = - this.actionSource === ActionSource.From ? getActionModeStartState : getActionModeEndState; - this.registerSubscription( - this.store - .select(shapeShifterSelector) - .subscribe( - ({ - vectorLayer, - blockLayerId, - isActionMode, - hover, - selections, - pairedSubPaths, - unpairedSubPath, - hiddenLayerIds, - selectedLayerIds, - subIdxWithError, - }) => { - this.vectorLayer = vectorLayer; - this.blockLayerId = blockLayerId; - this.isActionMode = isActionMode; - this.actionHover = hover; - this.actionSelections = selections; - this.pairedSubPaths = new Set(pairedSubPaths); - this.unpairedSubPath = unpairedSubPath; - this.hiddenLayerIds = hiddenLayerIds; - this.selectedLayerIds = selectedLayerIds; - this.subIdxWithError = subIdxWithError; - this.draw(); - }, - ), - ); - this.registerSubscription( - this.store.select(getActionMode).subscribe(mode => { - this.actionMode = mode; - const layer = this.activePathLayer; - if ( - this.actionMode === ActionMode.SplitCommands || - (this.actionMode === ActionMode.SplitSubPaths && - layer && - layer.isStroked() && - !layer.isFilled()) - ) { - const subIdxs = new Set(); - for (const s of this.actionSelections) { - subIdxs.add(s.subIdx); - } - this.segmentSplitter = new SegmentSplitter(this); - } else { - this.segmentSplitter = undefined; - } - this.selectionHelper = - this.actionMode === ActionMode.Selection ? new SelectionHelper(this) : undefined; - if (this.actionMode === ActionMode.PairSubPaths) { - this.pairSubPathHelper = new PairSubPathHelper(this); - const selections = this.actionSelections.filter(s => s.type === SelectionType.SubPath); - if (selections.length) { - const { source, subIdx } = selections[0]; - // TODO: avoid calling this in a subscription (should automatically do this) - this.actionModeService.setUnpairedSubPath({ source, subIdx }); - } - } else { - this.pairSubPathHelper = undefined; - } - this.shapeSplitter = - this.actionMode === ActionMode.SplitSubPaths && layer && layer.isFilled() - ? new ShapeSplitter(this) - : undefined; - this.currentHoverPreviewPath = undefined; - this.draw(); - }), - ); - const updateCurrentHoverFn = (hover: Hover | undefined) => { - let previewPath: Path; - if (this.vectorLayer && this.activePath && hover) { - // If the user is hovering over the inspector split button, then build - // a snapshot of what the path would look like after the action - // and display the result. - const mutator = this.activePath.mutate(); - const { type, subIdx, cmdIdx } = hover; - switch (type) { - case HoverType.Split: - previewPath = mutator.splitCommandInHalf(subIdx, cmdIdx).build(); - break; - case HoverType.Unsplit: - previewPath = mutator.unsplitCommand(subIdx, cmdIdx).build(); - break; - } - } - this.currentHoverPreviewPath = previewPath; - this.draw(); - }; - // TODO: avoid re-executing the draw by combining with the above subscriptions - this.registerSubscription( - this.store.select(getActionModeHover).subscribe(hover => { - if (!hover) { - // Clear the current hover. - updateCurrentHoverFn(undefined); - return; - } - if (hover.source !== this.actionSource && hover.type !== HoverType.Point) { - updateCurrentHoverFn(undefined); - return; - } - updateCurrentHoverFn(hover); - }), - ); - } - } - - private get overlayCtx() { - return this.$canvas.get(0).getContext('2d'); - } - - private get highlightLineWidth() { - return HIGHLIGHT_LINE_WIDTH / this.cssScale; - } - - private get minSnapThreshold() { - return MIN_SNAP_THRESHOLD / this.cssScale; - } - - private get highlightLineDash() { - return [HIGHLIGHT_LINE_DASH / this.cssScale, HIGHLIGHT_LINE_DASH / this.cssScale]; - } - - private get smallPointRadius() { - return SMALL_POINT_RADIUS / this.cssScale; - } - - private get mediumPointRadius() { - return MEDIUM_POINT_RADIUS / this.cssScale; - } - - private get splitPointRadius() { - return this.mediumPointRadius * SPLIT_POINT_RADIUS_FACTOR; - } - - private get selectedSegmentLineWidth() { - return HIGHLIGHT_LINE_WIDTH / this.cssScale / 1.9; - } - - private get unselectedSegmentLineWidth() { - return HIGHLIGHT_LINE_WIDTH / this.cssScale / 3; - } - - get dragTriggerTouchSlop() { - return DRAG_TRIGGER_TOUCH_SLOP / this.cssScale; - } - - // NOTE: only use this for action mode - get activePathLayer() { - if (!this.vectorLayer) { - return undefined; - } - return this.vectorLayer.findLayerById(this.blockLayerId) as MorphableLayer; - } - - // NOTE: only use this for action mode - get activePath() { - const layer = this.activePathLayer; - if (!layer) { - return undefined; - } - return layer.pathData; - } - - // @Override - protected onDimensionsChanged() { - const { w, h } = this.getViewport(); - this.$canvas.attr({ width: w * this.attrScale, height: h * this.attrScale }); - this.$canvas.css({ width: w * this.cssScale, height: h * this.cssScale }); - this.draw(); - } - - draw() { - const ctx = this.overlayCtx; - if (this.vectorLayer) { - const { w, h } = this.getViewport(); - ctx.save(); - ctx.scale(this.attrScale, this.attrScale); - ctx.clearRect(0, 0, w, h); - this.drawLayerSelections(ctx, this.vectorLayer); - this.drawHighlights(ctx); - ctx.restore(); - // Draw points in terms of physical pixels, not viewport pixels. - this.drawLabeledPoints(ctx); - this.drawDraggingPoints(ctx); - this.drawFloatingPreviewPoint(ctx); - this.drawFloatingSplitFilledPathPreviewPoints(ctx); - } - this.drawPixelGrid(ctx); - } - - // Recursively draws all layer selections to the canvas. - private drawLayerSelections(ctx: Context, curr: Layer) { - if (this.isActionMode) { - // Don't draw selections for hidden layers or while in action mode. - return; - } - if (this.selectedLayerIds.has(curr.id) || this.selectedBlockLayerIds.has(curr.id)) { - const root = this.vectorLayer; - const flattenedTransform = LayerUtil.getCanvasTransformForLayer(root, curr.id); - if (curr instanceof ClipPathLayer) { - if (curr.pathData && curr.pathData.getCommands().length) { - CanvasUtil.executeCommands(ctx, curr.pathData.getCommands(), flattenedTransform); - executeHighlights(ctx, HIGHLIGHT_COLOR, this.highlightLineWidth, this.highlightLineDash); - ctx.clip(); - } - } else if (curr instanceof PathLayer) { - if (curr.pathData && curr.pathData.getCommands().length) { - ctx.save(); - CanvasUtil.executeCommands(ctx, curr.pathData.getCommands(), flattenedTransform); - executeHighlights(ctx, HIGHLIGHT_COLOR, this.highlightLineWidth); - ctx.restore(); - } - } else if (curr instanceof VectorLayer || curr instanceof GroupLayer) { - const bounds = curr.bounds; - if (bounds) { - ctx.save(); - const { a, b, c, d, e, f } = flattenedTransform; - ctx.transform(a, b, c, d, e, f); - ctx.beginPath(); - ctx.rect(bounds.l, bounds.t, bounds.r - bounds.l, bounds.b - bounds.t); - executeHighlights(ctx, HIGHLIGHT_COLOR, this.highlightLineWidth); - ctx.restore(); - } - } - } - curr.children.forEach(child => this.drawLayerSelections(ctx, child)); - } - - // Draw any highlighted segments. - private drawHighlights(ctx: Context) { - if (!this.isActionMode || this.actionSource === ActionSource.Animated || !this.activePath) { - return; - } - - const flattenedTransform = LayerUtil.getCanvasTransformForLayer( - this.vectorLayer, - this.blockLayerId, - ); - const pathLayer = this.activePathLayer; - const activePath = pathLayer.pathData; - const currentHover = this.actionHover; - - if (this.selectionHelper) { - // Draw any highlighted subpaths. We'll highlight a subpath if a subpath - // selection or a point selection exists. - const selectedSubPaths = _(this.actionSelections as Selection[]) - .filter(s => { - return ( - s.source === this.actionSource && - (s.type === SelectionType.Point || s.type === SelectionType.SubPath) - ); - }) - .map(s => s.subIdx) - .uniq() - .map(subIdx => activePath.getSubPath(subIdx)) - .filter(subPath => !subPath.isCollapsing()) - .value(); - - for (const subPath of selectedSubPaths) { - // If the subpath has a split segment, highlight it in orange. Otherwise, - // use the default blue highlight color. - const isSplitSubPath = subPath.getCommands().some(c => c.isSplitSegment()); - const highlightColor = isSplitSubPath ? SPLIT_POINT_COLOR : HIGHLIGHT_COLOR; - CanvasUtil.executeCommands(ctx, subPath.getCommands(), flattenedTransform); - executeHighlights(ctx, highlightColor, this.selectedSegmentLineWidth); - } - - const segmentSelections = this.actionSelections - .filter(s => s.type === SelectionType.Segment) - .filter(s => s.source === this.actionSource) - .map(s => { - return { subIdx: s.subIdx, cmdIdx: s.cmdIdx }; - }); - const hover = currentHover; - if (hover && hover.source === this.actionSource && hover.type === HoverType.Segment) { - segmentSelections.push({ - subIdx: hover.subIdx, - cmdIdx: hover.cmdIdx, - }); - } - const segmentSelectionCmds = segmentSelections - .map(s => activePath.getCommand(s.subIdx, s.cmdIdx)) - .filter(cmd => cmd.isSplitSegment()); - CanvasUtil.executeCommands(ctx, segmentSelectionCmds, flattenedTransform); - executeHighlights(ctx, SPLIT_POINT_COLOR, this.selectedSegmentLineWidth); - - // Highlight any subpaths with errors. - if (this.subIdxWithError !== undefined) { - const cmds = activePath.getSubPath(this.subIdxWithError).getCommands(); - CanvasUtil.executeCommands(ctx, cmds, flattenedTransform); - executeHighlights(ctx, ERROR_COLOR, this.highlightLineWidth, this.highlightLineDash); - } - } else if (this.segmentSplitter && this.segmentSplitter.getProjectionOntoPath()) { - // Highlight the segment as the user hovers over it. - const { - subIdx, - cmdIdx, - projection: { d }, - } = this.segmentSplitter.getProjectionOntoPath(); - if (d < this.minSnapThreshold) { - CanvasUtil.executeCommands( - ctx, - [activePath.getCommand(subIdx, cmdIdx)], - flattenedTransform, - ); - executeHighlights(ctx, SPLIT_POINT_COLOR, this.selectedSegmentLineWidth); - } - } - - // Draw any existing split shape segments to the canvas. - const commands = _(activePath.getSubPaths() as SubPath[]) - .filter(s => !s.isCollapsing()) - .flatMap(s => s.getCommands() as Command[]) - .filter(c => c.isSplitSegment()) - .value(); - CanvasUtil.executeCommands(ctx, commands, flattenedTransform); - executeHighlights(ctx, SPLIT_POINT_COLOR, this.unselectedSegmentLineWidth); - - if (this.pairSubPathHelper) { - const currUnpair = this.unpairedSubPath; - if (currUnpair) { - // Draw the current unpaired subpath in orange, if it exists. - const { source, subIdx } = currUnpair; - if (source === this.actionSource) { - const subPath = activePath.getSubPath(subIdx); - CanvasUtil.executeCommands(ctx, subPath.getCommands(), flattenedTransform); - executeHighlights(ctx, SPLIT_POINT_COLOR, this.selectedSegmentLineWidth); - } - } - const pairedSubPaths = this.pairedSubPaths; - const hasHover = - currentHover && - currentHover.source === this.actionSource && - currentHover.type === HoverType.SubPath; - if (hasHover) { - pairedSubPaths.delete(currentHover.subIdx); - } - if (pairedSubPaths.size) { - // Draw any already paired subpaths in blue. - const pairedCmds = _.flatMap( - Array.from(pairedSubPaths), - subIdx => activePath.getSubPath(subIdx).getCommands() as Command[], - ); - CanvasUtil.executeCommands(ctx, pairedCmds, flattenedTransform); - executeHighlights(ctx, NORMAL_POINT_COLOR, this.selectedSegmentLineWidth); - } - if (hasHover) { - // Highlight the hover in orange, if it exists. - const hoverCmds = activePath.getSubPath(currentHover.subIdx).getCommands(); - CanvasUtil.executeCommands(ctx, hoverCmds, flattenedTransform); - executeHighlights(ctx, SPLIT_POINT_COLOR, this.selectedSegmentLineWidth); - } - } else if (this.shapeSplitter) { - // If we are splitting a filled subpath, draw the in progress drag segment. - const proj1 = this.shapeSplitter.getInitialProjectionOntoPath(); - const proj2 = this.shapeSplitter.getFinalProjectionOntoPath(); - if (proj1) { - // Draw a line from the starting projection to the final projection (or - // to the last known mouse location, if one doesn't exist). - const startPoint = applyGroupTransform(proj1.projection, flattenedTransform); - const endPoint = proj2 - ? applyGroupTransform(proj2.projection, flattenedTransform) - : this.shapeSplitter.getLastKnownMouseLocation(); - ctx.beginPath(); - ctx.moveTo(startPoint.x, startPoint.y); - ctx.lineTo(endPoint.x, endPoint.y); - executeHighlights(ctx, SPLIT_POINT_COLOR, this.selectedSegmentLineWidth); - } - if (!proj1 || proj2) { - // Highlight the segment as the user hovers over it. - const projectionOntoPath = this.shapeSplitter.getCurrentProjectionOntoPath(); - if (projectionOntoPath) { - const projection = projectionOntoPath.projection; - if (projection && projection.d < this.minSnapThreshold) { - const { subIdx, cmdIdx } = projectionOntoPath; - CanvasUtil.executeCommands( - ctx, - [activePath.getCommand(subIdx, cmdIdx)], - flattenedTransform, - ); - executeHighlights(ctx, SPLIT_POINT_COLOR, this.selectedSegmentLineWidth); - } - } - } - } - } - - // Draw any labeled points. - private drawLabeledPoints(ctx: Context) { - if (!this.isActionMode || this.actionSource === ActionSource.Animated || !this.activePath) { - return; - } - - const pathLayer = this.activePathLayer; - let path = pathLayer.pathData; - if (this.currentHoverPreviewPath) { - path = this.currentHoverPreviewPath; - } - - interface PointInfo { - cmd: Command; - subIdx: number; - cmdIdx: number; - } - - // Create a list of all path points in their normal order. - const pointInfos = _(path.getSubPaths() as SubPath[]) - .filter(s => !s.isCollapsing()) - .map((s, subIdx) => { - return s.getCommands().map((cmd, cmdIdx) => { - return { cmd, subIdx, cmdIdx } as PointInfo; - }); - }) - .flatMap(pis => pis) - .reverse() - .value(); - - const subPathSelections = this.actionSelections.filter(s => s.type === SelectionType.SubPath); - const pointSelections = this.actionSelections.filter(s => s.type === SelectionType.Point); - - // Remove all selected points from the list. - const isPointInfoSelectedFn = ({ subIdx, cmdIdx }: PointInfo) => { - return ( - subPathSelections.some(s => s.subIdx === subIdx) || - pointSelections.some(s => s.subIdx === subIdx && s.cmdIdx === cmdIdx) - ); - }; - const selectedPointInfos = _.remove(pointInfos, pi => isPointInfoSelectedFn(pi)); - // Remove any subpath points that share the same subIdx as an existing selection. - // We'll call these 'medium' points (i.e. labeled, but not selected), and we'll - // always draw selected points on top of medium points, and medium points - // on top of small points. - const isPointInfoAtLeastMediumFn = ({ subIdx }: PointInfo) => { - return ( - subPathSelections.some(s => s.subIdx === subIdx) || - pointSelections.some(s => s.subIdx === subIdx) - ); - }; - pointInfos.push(..._.remove(pointInfos, pi => isPointInfoAtLeastMediumFn(pi))); - pointInfos.push(...selectedPointInfos); - - const currentHover = this.actionHover; - - // Remove a hovering point, if one exists. - const hoveringPointInfos = _.remove(pointInfos, ({ subIdx, cmdIdx }: PointInfo) => { - const hover = currentHover; - return ( - hover && - hover.type === HoverType.Point && - hover.subIdx === subIdx && - hover.cmdIdx === cmdIdx - ); - }); - // Remove any subpath points that share the same subIdx as an existing hover. - const isPointInfoHoveringFn = ({ subIdx }: PointInfo) => { - const hover = currentHover; - return hover && hover.type !== HoverType.Segment && hover.subIdx === subIdx; - }; - // Similar to above, always draw hover points on top of subpath hover points. - pointInfos.push(..._.remove(pointInfos, pi => isPointInfoHoveringFn(pi))); - pointInfos.push(...hoveringPointInfos); - - const draggingIndex = - this.selectionHelper && this.selectionHelper.isDragTriggered() - ? this.selectionHelper.getDraggableSplitIndex() - : undefined; - - for (const { cmd, subIdx, cmdIdx } of pointInfos) { - if (draggingIndex && subIdx === draggingIndex.subIdx && cmdIdx === draggingIndex.cmdIdx) { - // Skip the currently dragged point. We'll draw that next. - continue; - } - let radius = this.smallPointRadius; - let text: string; - const isHovering = isPointInfoHoveringFn({ cmd, subIdx, cmdIdx }); - const isAtLeastMedium = isPointInfoAtLeastMediumFn({ cmd, subIdx, cmdIdx }); - if ((isAtLeastMedium || isHovering) && this.actionMode === ActionMode.Selection) { - radius = this.mediumPointRadius * SELECTED_POINT_RADIUS_FACTOR; - const isPointEnlargedFn = (source: ActionSource, sIdx: number, cIdx: number) => { - return pointSelections.some(s => { - return s.subIdx === sIdx && s.cmdIdx === cIdx && s.source === source; - }); - }; - if ( - (isHovering && cmdIdx === currentHover.cmdIdx) || - isPointEnlargedFn(ActionSource.From, subIdx, cmdIdx) || - isPointEnlargedFn(ActionSource.To, subIdx, cmdIdx) - ) { - radius /= SPLIT_POINT_RADIUS_FACTOR; - } - text = (cmdIdx + 1).toString(); - } - let color: string; - if (cmd.isSplitPoint()) { - radius *= SPLIT_POINT_RADIUS_FACTOR; - color = SPLIT_POINT_COLOR; - } else { - color = NORMAL_POINT_COLOR; - } - const flattenedTransform = LayerUtil.getCanvasTransformForLayer( - this.vectorLayer, - this.blockLayerId, - ); - executeLabeledPoint( - ctx, - this.attrScale, - applyGroupTransform(_.last(cmd.points), flattenedTransform), - radius, - color, - text, - ); - } - } - - // Draw any actively dragged points along the path in selection mode. - private drawDraggingPoints(ctx: Context) { - if (!this.isActionMode || this.actionSource === ActionSource.Animated || !this.activePath) { - return; - } - - if ( - this.actionMode !== ActionMode.Selection || - !this.selectionHelper || - !this.selectionHelper.isDragTriggered() - ) { - return; - } - const flattenedTransform = LayerUtil.getCanvasTransformForLayer( - this.vectorLayer, - this.blockLayerId, - ); - const projection = this.selectionHelper.getProjectionOntoPath().projection; - const point = - projection.d < this.minSnapThreshold - ? applyGroupTransform(projection, flattenedTransform) - : this.selectionHelper.getLastKnownMouseLocation(); - executeLabeledPoint(ctx, this.attrScale, point, this.splitPointRadius, SPLIT_POINT_COLOR); - } - - // Draw a floating point preview over the canvas in split commands mode - // and split subpaths mode for stroked paths. - private drawFloatingPreviewPoint(ctx: Context) { - if (!this.isActionMode || this.actionSource === ActionSource.Animated || !this.activePath) { - return; - } - - const pathLayer = this.activePathLayer; - if ( - (this.actionMode !== ActionMode.SplitCommands && - this.actionMode !== ActionMode.SplitSubPaths && - !pathLayer.isStroked()) || - !this.segmentSplitter || - !this.segmentSplitter.getProjectionOntoPath() - ) { - return; - } - const projection = this.segmentSplitter.getProjectionOntoPath().projection; - if (projection.d < this.minSnapThreshold) { - const flattenedTransform = LayerUtil.getCanvasTransformForLayer( - this.vectorLayer, - this.blockLayerId, - ); - executeLabeledPoint( - ctx, - this.attrScale, - applyGroupTransform(projection, flattenedTransform), - this.splitPointRadius, - SPLIT_POINT_COLOR, - ); - } - } - - // Draw the floating points on top of the drag line in split filled subpath mode. - private drawFloatingSplitFilledPathPreviewPoints(ctx: Context) { - if (!this.isActionMode || this.actionSource === ActionSource.Animated || !this.activePath) { - return; - } - if (this.actionMode !== ActionMode.SplitSubPaths || !this.shapeSplitter) { - return; - } - const flattenedTransform = LayerUtil.getCanvasTransformForLayer( - this.vectorLayer, - this.blockLayerId, - ); - const proj1 = this.shapeSplitter.getInitialProjectionOntoPath(); - if (proj1) { - const proj2 = this.shapeSplitter.getFinalProjectionOntoPath(); - executeLabeledPoint( - ctx, - this.attrScale, - applyGroupTransform(proj1.projection, flattenedTransform), - this.splitPointRadius, - SPLIT_POINT_COLOR, - ); - if (this.shapeSplitter.willFinalProjectionOntoPathCreateSplitPoint()) { - const endPoint = proj2 - ? applyGroupTransform(proj2.projection, flattenedTransform) - : this.shapeSplitter.getLastKnownMouseLocation(); - executeLabeledPoint( - ctx, - this.attrScale, - endPoint, - this.splitPointRadius, - SPLIT_POINT_COLOR, - ); - } - } else if (this.shapeSplitter.getCurrentProjectionOntoPath()) { - const projection = this.shapeSplitter.getCurrentProjectionOntoPath().projection; - if (projection.d < this.minSnapThreshold) { - executeLabeledPoint( - ctx, - this.attrScale, - applyGroupTransform(projection, flattenedTransform), - this.splitPointRadius, - SPLIT_POINT_COLOR, - ); - } - } - } - - // Draws the pixel grid on top of the canvas content. - private drawPixelGrid(ctx: Context) { - // Note that we draw the pixel grid in terms of physical pixels, - // not viewport pixels. - if (this.cssScale > 4) { - ctx.save(); - ctx.fillStyle = 'rgba(128, 128, 128, .25)'; - const devicePixelRatio = window.devicePixelRatio || 1; - const viewport = this.getViewport(); - for (let x = 1; x < viewport.w; x++) { - ctx.fillRect( - x * this.attrScale - devicePixelRatio / 2, - 0, - devicePixelRatio, - viewport.h * this.attrScale, - ); - } - for (let y = 1; y < viewport.h; y++) { - ctx.fillRect( - 0, - y * this.attrScale - devicePixelRatio / 2, - viewport.w * this.attrScale, - devicePixelRatio, - ); - } - ctx.restore(); - } - } - - // Called by the CanvasComponent. - onMouseDown(event: MouseEvent) { - const mouseDown = this.mouseEventToViewportCoords(event); - if (this.actionSource === ActionSource.Animated && !this.isActionMode) { - // Detect layer selections. - const hitLayer = this.hitTestForLayer(mouseDown); - const isMetaOrShiftPressed = - ShortcutService.isOsDependentModifierKey(event) || event.shiftKey; - if (hitLayer) { - this.layerTimelineService.selectLayer(hitLayer.id, !isMetaOrShiftPressed); - } else if (!isMetaOrShiftPressed) { - this.layerTimelineService.clearSelections(); - } - return; - } - if (this.actionSource === ActionSource.Animated) { - // Don't need to do anything for the animated canvas if we are in action mode. - return; - } - if (this.actionMode === ActionMode.Selection) { - this.selectionHelper.onMouseDown( - mouseDown, - event.shiftKey || ShortcutService.isOsDependentModifierKey(event), - ); - } else if (this.actionMode === ActionMode.PairSubPaths) { - this.pairSubPathHelper.onMouseDown( - mouseDown, - event.shiftKey || ShortcutService.isOsDependentModifierKey(event), - ); - } else if (this.actionMode === ActionMode.SplitCommands) { - this.segmentSplitter.onMouseDown(mouseDown); - } else if (this.actionMode === ActionMode.SplitSubPaths) { - const pathLayer = this.activePathLayer; - if (!pathLayer.isFilled() && pathLayer.isStroked()) { - this.segmentSplitter.onMouseDown(mouseDown); - } else { - this.shapeSplitter.onMouseDown(mouseDown); - } - } - } - - // Called by the CanvasComponent. - onMouseMove(event: MouseEvent) { - if (this.actionSource === ActionSource.Animated && !this.isActionMode) { - return; - } - const mouseMove = this.mouseEventToViewportCoords(event); - if (this.actionMode === ActionMode.Selection) { - this.selectionHelper.onMouseMove(mouseMove); - } else if (this.actionMode === ActionMode.PairSubPaths) { - this.pairSubPathHelper.onMouseMove(mouseMove); - } else if (this.actionMode === ActionMode.SplitCommands) { - this.segmentSplitter.onMouseMove(mouseMove); - } else if (this.actionMode === ActionMode.SplitSubPaths) { - const pathLayer = this.activePathLayer; - if (!pathLayer.isFilled() && pathLayer.isStroked()) { - this.segmentSplitter.onMouseMove(mouseMove); - } else { - this.shapeSplitter.onMouseMove(mouseMove); - } - } - } - - // Called by the CanvasComponent. - onMouseUp(event: MouseEvent) { - if (this.actionSource === ActionSource.Animated && !this.isActionMode) { - return; - } - const mouseUp = this.mouseEventToViewportCoords(event); - if (this.actionMode === ActionMode.Selection) { - this.selectionHelper.onMouseUp( - mouseUp, - event.shiftKey || ShortcutService.isOsDependentModifierKey(event), - ); - } else if (this.actionMode === ActionMode.PairSubPaths) { - this.pairSubPathHelper.onMouseUp(mouseUp); - } else if (this.actionMode === ActionMode.SplitCommands) { - this.segmentSplitter.onMouseUp(mouseUp); - } else if (this.actionMode === ActionMode.SplitSubPaths) { - const pathLayer = this.activePathLayer; - if (!pathLayer.isFilled() && pathLayer.isStroked()) { - this.segmentSplitter.onMouseUp(mouseUp); - } else { - this.shapeSplitter.onMouseUp(mouseUp); - } - } - } - - // Called by the CanvasComponent. - onMouseLeave(event: MouseEvent) { - if (this.actionSource === ActionSource.Animated && !this.isActionMode) { - return; - } - const mouseLeave = this.mouseEventToViewportCoords(event); - if (this.actionMode === ActionMode.Selection) { - // TODO: how to handle the case where the mouse leaves and re-enters mid-gesture? - this.selectionHelper.onMouseLeave(mouseLeave); - } else if (this.actionMode === ActionMode.PairSubPaths) { - this.pairSubPathHelper.onMouseLeave(mouseLeave); - } else if (this.actionMode === ActionMode.SplitCommands) { - this.segmentSplitter.onMouseLeave(mouseLeave); - } else if (this.actionMode === ActionMode.SplitSubPaths) { - const pathLayer = this.activePathLayer; - if (!pathLayer.isFilled() && pathLayer.isStroked()) { - this.segmentSplitter.onMouseLeave(mouseLeave); - } else { - this.shapeSplitter.onMouseLeave(mouseLeave); - } - } - this.actionModeService.clearHover(); - } - - private mouseEventToViewportCoords(event: MouseEvent) { - const canvasOffset = this.$canvas.offset(); - const x = (event.pageX - canvasOffset.left) / this.cssScale; - const y = (event.pageY - canvasOffset.top) / this.cssScale; - return { x, y }; - } - - private hitTestForLayer(point: Point) { - const root = this.vectorLayer; - if (!root) { - return undefined; - } - const recurseFn = (layer: Layer): Layer => { - if (this.hiddenLayerIds.has(layer.id)) { - return undefined; - } - // TODO: use a user-defined type check to confirm this layer is an instance of MorphableLayer - if ((layer instanceof PathLayer || layer instanceof ClipPathLayer) && layer.pathData) { - const canvasToLayerMatrix = LayerUtil.getCanvasTransformForLayer(root, layer.id).invert(); - if (!canvasToLayerMatrix) { - // Do nothing if matrix is non-invertible. - return undefined; - } - const transformedPoint = MathUtil.transformPoint(point, canvasToLayerMatrix); - let isSegmentInRangeFn: (distance: number, cmd: Command) => boolean; - isSegmentInRangeFn = distance => { - let maxDistance = 0; - if (layer instanceof PathLayer && layer.isStroked()) { - maxDistance = Math.max(this.minSnapThreshold, layer.strokeWidth / 2); - } - return distance <= maxDistance; - }; - const findShapesInRange = layer.isFilled(); - const hitResult = layer.pathData.hitTest(transformedPoint, { - isSegmentInRangeFn, - findShapesInRange, - }); - return hitResult.isHit ? layer : undefined; - } - // Use 'hitTestLayer || h' and not the other way around because of reverse z-order. - return layer.children.reduce((h, l) => recurseFn(l) || h, undefined); - }; - return recurseFn(root) as MorphableLayer; - } - - // NOTE: this should only be used in action mode - performHitTest(mousePoint: Point, opts: HitTestOpts = {}) { - const flattenedTransform = LayerUtil.getCanvasTransformForLayer( - this.vectorLayer, - this.blockLayerId, - ).invert(); - const transformedMousePoint = MathUtil.transformPoint(mousePoint, flattenedTransform); - let isPointInRangeFn: (distance: number, cmd: Command) => boolean; - if (!opts.noPoints) { - isPointInRangeFn = (distance, cmd) => { - const multiplyFactor = cmd.isSplitPoint() ? SPLIT_POINT_RADIUS_FACTOR : 1; - return distance <= this.mediumPointRadius * multiplyFactor; - }; - } - const pathLayer = this.vectorLayer.findLayerById(this.blockLayerId) as MorphableLayer; - if (!pathLayer.pathData) { - return { isHit: false } as HitResult; - } - let isSegmentInRangeFn: (distance: number, cmd: Command) => boolean; - if (!opts.noSegments) { - isSegmentInRangeFn = distance => { - let maxDistance = opts.withExtraSegmentPadding ? this.minSnapThreshold : 0; - if (pathLayer.isStroked()) { - maxDistance = Math.max(maxDistance, (pathLayer as PathLayer).strokeWidth / 2); - } - return distance <= maxDistance; - }; - } - const findShapesInRange = pathLayer.isFilled() && !opts.noShapes; - const restrictToSubIdx = opts.restrictToSubIdx; - return pathLayer.pathData.hitTest(transformedMousePoint, { - isPointInRangeFn, - isSegmentInRangeFn, - findShapesInRange, - restrictToSubIdx, - }); - } -} - -function executeHighlights( - ctx: Context, - color: string, - lineWidth: number, - lineDash: number[] = [], -) { - ctx.save(); - ctx.setLineDash(lineDash); - ctx.lineCap = 'round'; - ctx.strokeStyle = color; - ctx.lineWidth = lineWidth; - ctx.stroke(); - ctx.restore(); -} - -// Draws a labeled point with optional text. -function executeLabeledPoint( - ctx: Context, - attrScale: number, - point: Point, - radius: number, - color: string, - text?: string, -) { - // Convert the point and the radius to physical pixel coordinates. - // We do this to avoid fractional font sizes less than 1px, which - // show up OK on Chrome but not on Firefox or Safari. - point = MathUtil.transformPoint(point, Matrix.scaling(attrScale, attrScale)); - radius *= attrScale; - - ctx.save(); - ctx.beginPath(); - ctx.arc(point.x, point.y, radius * POINT_BORDER_FACTOR, 0, 2 * Math.PI, false); - ctx.fillStyle = POINT_BORDER_COLOR; - ctx.fill(); - - ctx.beginPath(); - ctx.arc(point.x, point.y, radius, 0, 2 * Math.PI, false); - ctx.fillStyle = color; - ctx.fill(); - - if (text) { - ctx.beginPath(); - ctx.fillStyle = POINT_TEXT_COLOR; - ctx.font = radius + 'px Roboto, Helvetica Neue, sans-serif'; - const width = ctx.measureText(text).width; - // TODO: is there a better way to get the height? - const height = ctx.measureText('o').width; - ctx.fillText(text, point.x - width / 2, point.y + height / 2); - ctx.fill(); - } - ctx.restore(); -} - -// Takes a path point and transforms it so that its coordinates are in terms -// of the VectorLayer's viewport coordinates. -function applyGroupTransform(mousePoint: Point, transform: Matrix) { - return MathUtil.transformPoint(mousePoint, transform); -} - -interface HitTestOpts { - readonly noPoints?: boolean; - readonly noSegments?: boolean; - readonly noShapes?: boolean; - readonly restrictToSubIdx?: ReadonlyArray; - readonly withExtraSegmentPadding?: boolean; -} diff --git a/src/app/pages/editor/components/canvas/canvaspaper.directive.ts b/src/app/pages/editor/components/canvas/canvaspaper.directive.ts deleted file mode 100644 index 33270d44..00000000 --- a/src/app/pages/editor/components/canvas/canvaspaper.directive.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { AfterViewInit, Directive, ElementRef, Input, OnDestroy } from '@angular/core'; -import { ActionSource } from 'app/pages/editor/model/actionmode'; -import { DestroyableMixin } from 'app/pages/editor/scripts/mixins'; -import { PaperProject } from 'app/pages/editor/scripts/paper'; -import { PaperService } from 'app/pages/editor/services'; -import { State, Store } from 'app/pages/editor/store'; -import * as $ from 'jquery'; - -import { CanvasLayoutMixin } from './CanvasLayoutMixin'; - -@Directive({ selector: '[appCanvasPaper]' }) -export class CanvasPaperDirective extends CanvasLayoutMixin(DestroyableMixin()) - implements AfterViewInit, OnDestroy { - @Input() - actionSource: ActionSource; - private readonly $canvas: JQuery; - private paperProject: PaperProject; - - constructor( - elementRef: ElementRef, - private readonly ps: PaperService, - private readonly store: Store, - ) { - super(); - this.$canvas = $(elementRef.nativeElement) as JQuery; - } - - ngAfterViewInit() { - this.paperProject = new PaperProject(this.$canvas.get(0), this.ps, this.store); - } - - ngOnDestroy() { - this.paperProject.remove(); - } - - // @Override - protected onDimensionsChanged() { - const { w, h } = this.getViewport(); - const scale = this.cssScale; - this.$canvas.css({ width: w * scale, height: h * scale }); - this.paperProject.setDimensions(w, h, w * scale, h * scale); - } -} diff --git a/src/app/pages/editor/components/canvas/canvasruler.directive.ts b/src/app/pages/editor/components/canvas/canvasruler.directive.ts deleted file mode 100644 index d6bbb9cf..00000000 --- a/src/app/pages/editor/components/canvas/canvasruler.directive.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { Directive, ElementRef, Input } from '@angular/core'; -import { MathUtil, Point } from 'app/pages/editor/scripts/common'; -import { ThemeService } from 'app/pages/editor/services'; -import * as $ from 'jquery'; - -import { CanvasLayoutMixin } from './CanvasLayoutMixin'; - -// All dimensions are in CSS pixels. -const RULER_SIZE = 32; -const EXTRA_RULER_PADDING = 12; -const GRID_INTERVALS_PX: ReadonlyArray = [1, 2, 4, 8, 16, 24, 48, 100, 100, 250]; -const LABEL_OFFSET = 12; -const TICK_SIZE = 6; - -@Directive({ selector: '[appCanvasRuler]' }) -export class CanvasRulerDirective extends CanvasLayoutMixin() { - @Input() - orientation: Orientation; - - private readonly $canvas: JQuery; - - // The current mouse point in viewport coordinates. - private vpMousePoint: Point; - - constructor(elementRef: ElementRef, private readonly themeService: ThemeService) { - super(); - this.$canvas = $(elementRef.nativeElement) as JQuery; - } - - // @Override - protected onDimensionsChanged() { - this.draw(); - } - - // @Override - protected onZoomPanChanged() { - this.draw(); - } - - hideMouse() { - if (this.vpMousePoint) { - this.vpMousePoint = undefined; - this.draw(); - } - } - - // TODO: need to transform mouse point to account for zoom and translation - showMouse(mousePoint: Point) { - if (!this.vpMousePoint || !MathUtil.arePointsEqual(this.vpMousePoint, mousePoint)) { - this.vpMousePoint = mousePoint; - this.draw(); - } - } - - private draw() { - const isHorizontal = this.orientation === 'horizontal'; - - const viewport = this.getViewport(); - const zoom = this.getZoom(); - const { cssScale } = this; - const width = isHorizontal - ? viewport.w * cssScale * zoom + EXTRA_RULER_PADDING * 2 - : RULER_SIZE; - const height = isHorizontal - ? RULER_SIZE - : viewport.h * cssScale * zoom + EXTRA_RULER_PADDING * 2; - this.$canvas.css({ width, height }); - this.$canvas.attr({ width: width * devicePixelRatio, height: height * devicePixelRatio }); - - const ctx = this.$canvas.get(0).getContext('2d'); - ctx.scale(devicePixelRatio, devicePixelRatio); - const { tx, ty } = this.getTranslation(); - ctx.translate( - isHorizontal ? tx + EXTRA_RULER_PADDING : 0, - isHorizontal ? 0 : ty + EXTRA_RULER_PADDING, - ); - - const widthMinusPadding = width - EXTRA_RULER_PADDING * 2; - const heightMinusPadding = height - EXTRA_RULER_PADDING * 2; - const rulerZoom = Math.max( - 1, - isHorizontal ? widthMinusPadding / viewport.w : heightMinusPadding / viewport.h, - ); - - // TODO: change the grid spacing depending on the current zoom? - // Compute grid spacing (40 = minimum grid spacing in pixels). - let interval = 0; - let spacingViewportPx = GRID_INTERVALS_PX[interval]; - while (spacingViewportPx * rulerZoom < 40 || interval >= GRID_INTERVALS_PX.length) { - interval++; - spacingViewportPx = GRID_INTERVALS_PX[interval]; - } - - const spacingRulerPx = spacingViewportPx * rulerZoom; - - // Text labels. - ctx.fillStyle = this.themeService.getDisabledTextColor(); - ctx.font = '10px Roboto, Helvetica Neue, sans-serif'; - if (isHorizontal) { - ctx.textBaseline = 'alphabetic'; - ctx.textAlign = 'center'; - const minX = -tx; - const maxX = minX + widthMinusPadding / zoom; - for ( - let x = 0, t = 0; - MathUtil.round(x) <= MathUtil.round(width - EXTRA_RULER_PADDING * 2); - x += spacingRulerPx, t += spacingViewportPx - ) { - if (minX <= x && x <= maxX) { - ctx.fillText(t.toString(), x, height - LABEL_OFFSET); - ctx.fillRect(x - 0.5, height - TICK_SIZE, 1, TICK_SIZE); - } - } - } else { - ctx.textBaseline = 'middle'; - ctx.textAlign = 'right'; - const minY = -ty; - const maxY = minY + heightMinusPadding / zoom; - for ( - let y = 0, t = 0; - MathUtil.round(y) <= MathUtil.round(height - EXTRA_RULER_PADDING * 2); - y += spacingRulerPx, t += spacingViewportPx - ) { - if (minY <= y && y <= maxY) { - ctx.fillText(t.toString(), width - LABEL_OFFSET, y); - ctx.fillRect(width - TICK_SIZE, y - 0.5, TICK_SIZE, 1); - } - } - } - - if (this.vpMousePoint) { - const { x, y } = this.vpMousePoint; - ctx.fillStyle = this.themeService.getSecondaryTextColor(); - if (isHorizontal) { - ctx.fillText(x.toString(), x * rulerZoom, height - LABEL_OFFSET); - } else { - ctx.fillText(y.toString(), width - LABEL_OFFSET, y * rulerZoom); - } - } - } -} - -export type Orientation = 'horizontal' | 'vertical'; diff --git a/src/app/pages/editor/components/canvas/index.ts b/src/app/pages/editor/components/canvas/index.ts deleted file mode 100644 index 521931ae..00000000 --- a/src/app/pages/editor/components/canvas/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { CanvasComponent } from './canvas.component'; -export { CanvasContainerDirective } from './canvascontainer.directive'; -export { CanvasLayersDirective } from './canvaslayers.directive'; -export { CanvasOverlayDirective } from './canvasoverlay.directive'; -export { CanvasRulerDirective } from './canvasruler.directive'; -export { CanvasPaperDirective } from './canvaspaper.directive'; diff --git a/src/app/pages/editor/components/dialogs/_dialog-theme.scss b/src/app/pages/editor/components/dialogs/_dialog-theme.scss deleted file mode 100644 index 8172bae2..00000000 --- a/src/app/pages/editor/components/dialogs/_dialog-theme.scss +++ /dev/null @@ -1,12 +0,0 @@ -@mixin ss-dialog-theme($theme) { - $accent: map-get($theme, accent); - $foreground: map-get($theme, ss-foreground); - mat-dialog-actions { - button { - color: mat-color($accent); - } - } - mat-dialog-content { - color: mat-color($foreground, secondary-text); - } -} diff --git a/src/app/pages/editor/components/dialogs/confirmdialog.component.scss b/src/app/pages/editor/components/dialogs/confirmdialog.component.scss deleted file mode 100644 index ac45b95c..00000000 --- a/src/app/pages/editor/components/dialogs/confirmdialog.component.scss +++ /dev/null @@ -1,10 +0,0 @@ -button { - text-transform: uppercase; - &:mat-dialog-close { - margin-right: 8px; - } -} - -mat-dialog-actions { - min-width: 220px; -} diff --git a/src/app/pages/editor/components/dialogs/confirmdialog.component.ts b/src/app/pages/editor/components/dialogs/confirmdialog.component.ts deleted file mode 100644 index 56792bb4..00000000 --- a/src/app/pages/editor/components/dialogs/confirmdialog.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Component, Inject } from '@angular/core'; -import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material'; - -@Component({ - selector: 'app-confirmdialog', - template: ` - {{ this.data.title }} - -

{{ this.data.message }}

-
- - - - - - `, - styleUrls: ['./confirmdialog.component.scss'], -}) -export class ConfirmDialogComponent { - constructor( - readonly dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) readonly data: { title: string; message: string }, - ) {} -} diff --git a/src/app/pages/editor/components/dialogs/demodialog.component.scss b/src/app/pages/editor/components/dialogs/demodialog.component.scss deleted file mode 100644 index 615f0f65..00000000 --- a/src/app/pages/editor/components/dialogs/demodialog.component.scss +++ /dev/null @@ -1,19 +0,0 @@ -button { - text-transform: uppercase; - &:mat-dialog-close { - margin-right: 8px; - } -} - -mat-dialog-actions { - min-width: 220px; -} - -.dialog-radio-group { - display: inline-flex; - flex-direction: column; -} - -.dialog-radio-button { - margin-bottom: 16px; -} diff --git a/src/app/pages/editor/components/dialogs/demodialog.component.ts b/src/app/pages/editor/components/dialogs/demodialog.component.ts deleted file mode 100644 index c5dc1bae..00000000 --- a/src/app/pages/editor/components/dialogs/demodialog.component.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Component } from '@angular/core'; -import { MatDialogRef } from '@angular/material'; -import { DEMO_INFOS } from 'app/pages/editor/scripts/demos'; - -@Component({ - selector: 'app-demodialog', - template: ` - Choose a demo - - - {{ demoInfo.title }} - - - - - - - - `, - styleUrls: ['./demodialog.component.scss'], -}) -export class DemoDialogComponent { - readonly demoInfos = DEMO_INFOS; - selectedDemoInfo = DEMO_INFOS[0]; - - constructor(readonly dialogRef: MatDialogRef) {} -} diff --git a/src/app/pages/editor/components/dialogs/dialog.service.ts b/src/app/pages/editor/components/dialogs/dialog.service.ts deleted file mode 100644 index 74b2ff5f..00000000 --- a/src/app/pages/editor/components/dialogs/dialog.service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Injectable } from '@angular/core'; -import { MatDialog, MatDialogConfig } from '@angular/material'; -import { ConfirmDialogComponent } from 'app/pages/editor/components/dialogs/confirmdialog.component'; -import { DemoDialogComponent } from 'app/pages/editor/components/dialogs/demodialog.component'; -import { - DropFilesAction, - DropFilesDialogComponent, -} from 'app/pages/editor/components/dialogs/dropfilesdialog.component'; -import { DemoInfo } from 'app/pages/editor/scripts/demos'; -import { Observable } from 'rxjs'; - -@Injectable({ providedIn: 'root' }) -export class DialogService { - constructor(private readonly dialog: MatDialog) {} - - confirm(title: string, message: string): Observable { - const config = new MatDialogConfig(); - config.data = { title, message }; - return this.dialog.open(ConfirmDialogComponent, config).afterClosed(); - } - - pickDemo(): Observable { - return this.dialog.open(DemoDialogComponent, new MatDialogConfig()).afterClosed(); - } - - dropFiles(): Observable { - return this.dialog.open(DropFilesDialogComponent, new MatDialogConfig()).afterClosed(); - } -} diff --git a/src/app/pages/editor/components/dialogs/dropfilesdialog.component.scss b/src/app/pages/editor/components/dialogs/dropfilesdialog.component.scss deleted file mode 100644 index ac45b95c..00000000 --- a/src/app/pages/editor/components/dialogs/dropfilesdialog.component.scss +++ /dev/null @@ -1,10 +0,0 @@ -button { - text-transform: uppercase; - &:mat-dialog-close { - margin-right: 8px; - } -} - -mat-dialog-actions { - min-width: 220px; -} diff --git a/src/app/pages/editor/components/dialogs/dropfilesdialog.component.ts b/src/app/pages/editor/components/dialogs/dropfilesdialog.component.ts deleted file mode 100644 index 45e3c096..00000000 --- a/src/app/pages/editor/components/dialogs/dropfilesdialog.component.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Component } from '@angular/core'; -import { MatDialogRef } from '@angular/material'; - -export enum DropFilesAction { - AddToWorkspace = 1, - ResetWorkspace, -} - -@Component({ - selector: 'app-dropfilesdialog', - template: ` - Start from scratch? - -

Do you want to start from scratch or add the imported layers to the existing animation?

-
- - - - - - - `, - styleUrls: ['./dropfilesdialog.component.scss'], -}) -export class DropFilesDialogComponent { - readonly ADD_TO_WORKSPACE = DropFilesAction.AddToWorkspace; - readonly RESET_WORKSPACE = DropFilesAction.ResetWorkspace; - - constructor(readonly dialogRef: MatDialogRef) {} -} diff --git a/src/app/pages/editor/components/dialogs/index.ts b/src/app/pages/editor/components/dialogs/index.ts deleted file mode 100644 index fd30b26b..00000000 --- a/src/app/pages/editor/components/dialogs/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { ConfirmDialogComponent } from './confirmdialog.component'; -export { DemoDialogComponent } from './demodialog.component'; -export { DropFilesDialogComponent, DropFilesAction } from './dropfilesdialog.component'; -export { DialogService } from './dialog.service'; diff --git a/src/app/pages/editor/components/layertimeline/_layerlisttree-theme.scss b/src/app/pages/editor/components/layertimeline/_layerlisttree-theme.scss deleted file mode 100644 index 3a76a209..00000000 --- a/src/app/pages/editor/components/layertimeline/_layerlisttree-theme.scss +++ /dev/null @@ -1,51 +0,0 @@ -@mixin ss-layerlisttree-theme($theme) { - $foreground: map-get($theme, ss-foreground); - $background: map-get($theme, ss-background); - $accent: map-get($theme, accent); - $is-dark: map-get($theme, is-dark); - .slt-layer { - color: mat-color($foreground, secondary-text); - &:focus { - box-shadow: 0 0 0 1px mat-color($accent) inset; - } - &.is-selected { - background-color: mat-color($accent); - color: #fff; - &, - & mat-icon { - color: #fff; - } - } - &.is-hovered { - box-shadow: 0 0 0 1px mat-color($accent) inset; - } - } - .slt-properties { - background-color: mat-color($background, if($is-dark, base50, base)); - box-shadow: 0 0 0 1px mat-color($foreground, divider) inset; - } - .slt-property { - color: mat-color($foreground, secondary-text); - &:not(:last-child) { - box-shadow: 0 -1px 0 mat-color($foreground, divider) inset; - } - button { - &[mat-icon-button] { - mat-icon { - color: mat-color($foreground, disabled-text); - } - } - &[mat-icon-button][disabled] { - mat-icon { - color: mat-color($foreground, disabled-text, 0.5); - } - } - } - } - .slt-layers-list-drag-indicator { - background-color: mat-color($accent); - &::before { - background-color: mat-color($accent); - } - } -} diff --git a/src/app/pages/editor/components/layertimeline/_layertimeline-theme.scss b/src/app/pages/editor/components/layertimeline/_layertimeline-theme.scss deleted file mode 100644 index 7ee0218a..00000000 --- a/src/app/pages/editor/components/layertimeline/_layertimeline-theme.scss +++ /dev/null @@ -1,74 +0,0 @@ -@mixin ss-layertimeline-theme($theme) { - $foreground: map-get($theme, ss-foreground); - $background: map-get($theme, ss-background); - $accent: map-get($theme, accent); - .studio-layer-timeline { - $headerHeight: 40px; - background-color: mat-color($background, base100); - .slt-layers { - .slt-header { - .slt-layers-menu-group-button { - color: mat-color($foreground, secondary-text); - &:focus { - background-color: mat-color($foreground, divider); - } - &.is-disabled { - color: mat-color($foreground, disabled-text); - } - } - button { - &[mat-icon-button] { - mat-icon { - color: mat-color($foreground, secondary-text); - } - } - &[mat-icon-button][disabled] { - mat-icon { - color: mat-color($foreground, disabled-text); - } - } - } - } - .slt-layers-list-drag-indicator { - background-color: mat-color($accent); - &::before { - background-color: mat-color($accent); - } - } - } - .slt-layers-empty { - color: mat-color($foreground, disabled-text); - } - .slt-timeline { - .slt-timeline-animation { - box-shadow: 4px 0 0 mat-color(mat-palette($mat-grey, 500)), -4px 0 0 mat-color(mat-palette($mat-grey, 500)); - &.is-selected { - background-color: mat-color($accent); - } - } - .slt-header { - .slt-timeline-animation-meta { - &.is-selected, - &.is-selected .slt-timeline-animation-name { - background-color: mat-color($accent, darker); - color: #fff; - } - &.is-disabled { - cursor: default; - } - } - .slt-timeline-animation-name { - color: mat-color($foreground, primary-text); - margin-right: 4px; - font-weight: 500; - } - .slt-timeline-header-grid { - background-color: mat-color($background, base100); - } - } - } - .slt-header { - color: mat-color($foreground, secondary-text); - } - } -} diff --git a/src/app/pages/editor/components/layertimeline/_timelineanimationrow-theme.scss b/src/app/pages/editor/components/layertimeline/_timelineanimationrow-theme.scss deleted file mode 100644 index 9659dff5..00000000 --- a/src/app/pages/editor/components/layertimeline/_timelineanimationrow-theme.scss +++ /dev/null @@ -1,27 +0,0 @@ -@mixin ss-timelineanimationrow-theme($theme) { - $foreground: map-get($theme, ss-foreground); - $background: map-get($theme, ss-background); - $accent: map-get($theme, accent); - $is-dark: map-get($theme, is-dark); - .slt-properties { - background-color: mat-color($background, if($is-dark, base50, base)); - box-shadow: 0 0 0 1px mat-color($foreground, divider) inset; - .slt-property { - &:not(:last-child) { - box-shadow: 0 -1px 0 mat-color($foreground, divider) inset; - } - .slt-timeline-block { - background-color: mat-color(mat-palette(if($is-dark, $mat-indigo, $mat-green), 200)); - &.is-selected { - background-color: mat-color($accent); - } - &.has-error { - background-color: mat-color(mat-palette($mat-red, A200)); - } - &.is-selected-with-error { - background-color: mat-color(mat-palette($mat-red, A700)); - } - } - } - } -} diff --git a/src/app/pages/editor/components/layertimeline/constants.ts b/src/app/pages/editor/components/layertimeline/constants.ts deleted file mode 100644 index ea97bcca..00000000 --- a/src/app/pages/editor/components/layertimeline/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const TIMELINE_ANIMATION_PADDING = 20; // 20px diff --git a/src/app/pages/editor/components/layertimeline/index.ts b/src/app/pages/editor/components/layertimeline/index.ts deleted file mode 100644 index 0d1a612a..00000000 --- a/src/app/pages/editor/components/layertimeline/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { LayerListTreeComponent } from './layerlisttree.component'; -export { LayerTimelineComponent } from './layertimeline.component'; -export { LayerTimelineGridDirective } from './layertimelinegrid.directive'; -export { TimelineAnimationRowComponent } from './timelineanimationrow.component'; diff --git a/src/app/pages/editor/components/layertimeline/layerlisttree.component.html b/src/app/pages/editor/components/layertimeline/layerlisttree.component.html deleted file mode 100644 index b79763c3..00000000 --- a/src/app/pages/editor/components/layertimeline/layerlisttree.component.html +++ /dev/null @@ -1,126 +0,0 @@ - -
- - - - - - {{ this.layer.name }} - - - - - - - - - - - - - - - -
- - -
-
- {{ propertyName }} - -
-
- - -
    -
  • - - -
  • -
-
\ No newline at end of file diff --git a/src/app/pages/editor/components/layertimeline/layerlisttree.component.scss b/src/app/pages/editor/components/layertimeline/layerlisttree.component.scss deleted file mode 100644 index 9f09349c..00000000 --- a/src/app/pages/editor/components/layertimeline/layerlisttree.component.scss +++ /dev/null @@ -1,193 +0,0 @@ -:host { - user-select: none; -} - -ul { - margin: 0; - padding: 0; -} - -li { - list-style: none; -} - -.slt-layer { - box-sizing: border-box; - height: 20px; - display: flex; - flex-direction: row; - align-items: center; - padding: 2px; - font-size: 12px; - cursor: pointer; - outline: none; - border-radius: 2px 0 0 2px; - &.is-selected { - box-shadow: none; - } - &.is-disabled { - cursor: default; - } - span.slt-layer-id-text { - flex: 1 1 0; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - margin-left: 4px; - } - mat-icon { - width: 16px; - height: 16px; - font-size: 16px; - } - button { - line-height: 16px; - height: 20px; - width: 20px; - min-height: 20px; - &[mat-icon-button] { - mat-icon { - line-height: normal; - vertical-align: top; - } - } - } -} - -.slt-layer-type-group, -.slt-layer-type-vector { - font-weight: 500; -} - -.slt-children { - padding: 0; - margin: 0 0 0 20px; -} - -@mixin layer-list-button { - margin: 0; - padding: 2px; - height: 20px; - width: 20px; - line-height: 16px; - min-height: 20px; - mat-icon { - vertical-align: top; - } -} - -.slt-layer:hover { - .slt-layer-action-button { - &[disabled].is-checked { - opacity: 0; - } - &:not([disabled]) { - opacity: 1; - } - } -} - -.slt-layer.is-selected { - .slt-layer-action-button { - opacity: 1; - } -} - -.slt-layer-expanded-toggle { - @include layer-list-button; - visibility: hidden; - &.is-visible { - visibility: visible; - } -} - -.slt-layer-id { - flex: 1 1 0; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; -} - -.slt-layer-action-button { - @include layer-list-button; - opacity: 0.2; - &[disabled] { - opacity: 0; - } -} - -mat-menu { - display: flex; -} - -.slt-layer-visibility-toggle { - &.is-checked { - opacity: 0; - } - &:not(.is-checked) { - opacity: 0.7; - } -} - -.slt-layer-more-actions { - opacity: 0; -} - -.slt-layer-type-group { - font-weight: 500; -} - -.slt-properties { - margin-top: 4px; - margin-bottom: 4px; - margin-left: 40px; // indent by both the icon (20px) and expand toggle (20px) - margin-right: -2px; // hide inset shadow - padding-right: 2px; // offset margin - border-radius: 2px; -} - -.slt-property { - padding-left: 8px; - font-size: 12px; - .slt-property-name { - flex: 1 1 0; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - } - button { - margin: 2px; - padding: 2px; - line-height: 16px; - height: 20px; - width: 20px; - min-height: 20px; - &[mat-icon-button] { - mat-icon { - width: 16px; - height: 16px; - font-size: 16px; - line-height: normal; - vertical-align: top; - } - } - } -} - -.slt-layers-list-drag-indicator { - position: absolute; - height: 2px; - left: 0; - right: 0; - margin-top: -1px; - pointer-events: none; - &::before { - position: absolute; - content: ''; - left: -4px; - top: -3px; - height: 8px; - width: 8px; - border-radius: 50%; - } -} diff --git a/src/app/pages/editor/components/layertimeline/layerlisttree.component.ts b/src/app/pages/editor/components/layertimeline/layerlisttree.component.ts deleted file mode 100644 index 7f1c0631..00000000 --- a/src/app/pages/editor/components/layertimeline/layerlisttree.component.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - OnInit, - Output, -} from '@angular/core'; -import { ClipPathLayer, GroupLayer, Layer, PathLayer, VectorLayer } from 'app/pages/editor/model/layers'; -import { Animation, PathAnimationBlock } from 'app/pages/editor/model/timeline'; -import { ModelUtil } from 'app/pages/editor/scripts/common'; -import { ActionModeService } from 'app/pages/editor/services'; -import { State, Store } from 'app/pages/editor/store'; -import { getLayerListTreeState } from 'app/pages/editor/store/common/selectors'; -import * as _ from 'lodash'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; - -@Component({ - selector: 'app-layerlisttree', - templateUrl: './layerlisttree.component.html', - styleUrls: ['./layerlisttree.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class LayerListTreeComponent implements OnInit, Callbacks { - layerModel$: Observable; - - @Input() layer: Layer; - - // MouseEvents from this layer (or children layers further down the tree) - // are recursively handled by parent components until they reach - // the LayerTimelineComponent. - @Output() layerClick = new EventEmitter(); - @Output() layerDoubleClick = new EventEmitter(); - @Output() layerMouseDown = new EventEmitter(); - @Output() layerToggleExpanded = new EventEmitter(); - @Output() layerToggleVisibility = new EventEmitter(); - @Output() addTimelineBlockClick = new EventEmitter(); - @Output() convertToClipPathClick = new EventEmitter(); - @Output() convertToPathClick = new EventEmitter(); - @Output() flattenGroupClick = new EventEmitter(); - - constructor( - private readonly store: Store, - private readonly actionModeService: ActionModeService, - ) {} - - ngOnInit() { - this.layerModel$ = this.store.select(getLayerListTreeState).pipe( - map( - ({ - animation, - selectedLayerIds, - collapsedLayerIds, - hiddenLayerIds, - hoveredLayerId, - isActionMode, - }) => { - const isExpandable = this.isLayerExpandable(); - const availablePropertyNames = Array.from( - ModelUtil.getAvailablePropertyNamesForLayer(this.layer, animation), - ); - const getExistingPropertyNamesFn = (layerId: string) => { - return _.keys(ModelUtil.getOrderedBlocksByPropertyByLayer(animation)[layerId]); - }; - const existingPropertyNames = getExistingPropertyNamesFn(this.layer.id); - const canBeConvertedToPath = this.layer instanceof ClipPathLayer; - // We can't convert a path into a clip path if it has incompatible animation blocks. - const canBeConvertedToClipPath = - this.layer instanceof PathLayer && - // TODO: comparing the sets of all animatable properties for each layer type would be more robust - !animation.blocks.some(b => !(b instanceof PathAnimationBlock)); - const canBeFlattened = - this.layer instanceof GroupLayer && - this.layer.children.length > 0 && - // TODO: allow merging groups w/ existing blocks in some cases? - existingPropertyNames.length === 0 && - this.layer.children.every(l => { - return ( - l instanceof PathLayer || - l instanceof ClipPathLayer || - // TODO: allow merging groups into groups w/ existing blocks in some cases? - getExistingPropertyNamesFn(l.id).length === 0 - ); - }); - return { - animation, - isSelected: selectedLayerIds.has(this.layer.id), - isHovered: hoveredLayerId === this.layer.id, - isExpandable, - isExpanded: !collapsedLayerIds.has(this.layer.id), - isVisible: !hiddenLayerIds.has(this.layer.id), - availablePropertyNames, - existingPropertyNames, - isActionMode, - canBeConvertedToClipPath, - canBeConvertedToPath, - canBeFlattened, - }; - }, - ), - ); - } - - // @Override - onLayerClick(event: MouseEvent, layer: Layer) { - event.stopPropagation(); - if (!this.actionModeService.isActionMode()) { - this.layerClick.emit({ event, layer }); - } - } - - // @Override - onLayerMouseDown(event: MouseEvent, layer: Layer) { - if (!this.actionModeService.isActionMode()) { - this.layerMouseDown.emit({ event, layer }); - } - } - - // @Override - onLayerToggleExpanded(event: MouseEvent, layer: Layer) { - event.stopPropagation(); - if (this.isLayerExpandable()) { - this.layerToggleExpanded.emit({ event, layer }); - } - } - - // @Override - onLayerToggleVisibility(event: MouseEvent, layer: Layer) { - event.stopPropagation(); - if (!this.actionModeService.isActionMode()) { - this.layerToggleVisibility.emit({ event, layer }); - } - } - - // @Override - onAddTimelineBlockClick(event: MouseEvent, layer: Layer, propertyName: string) { - if (!this.actionModeService.isActionMode()) { - this.addTimelineBlockClick.emit({ event, layer, propertyName }); - } - } - - // @Override - onConvertToClipPathClick(event: MouseEvent, layer: Layer) { - if (!this.actionModeService.isActionMode()) { - this.convertToClipPathClick.emit({ event, layer }); - } - } - - // @Override - onConvertToPathClick(event: MouseEvent, layer: Layer) { - if (!this.actionModeService.isActionMode()) { - this.convertToPathClick.emit({ event, layer }); - } - } - - // @Override - onFlattenGroupClick(event: MouseEvent, layer: Layer) { - if (!this.actionModeService.isActionMode()) { - this.flattenGroupClick.emit({ event, layer }); - } - } - - // Used by *ngFor loop. - trackLayerFn(index: number, layer: Layer) { - return layer.id; - } - - private isLayerExpandable() { - return this.layer instanceof VectorLayer || this.layer instanceof GroupLayer; - } -} - -export interface Callbacks { - onLayerClick(event: MouseEvent, layer: Layer): void; - onLayerMouseDown(event: MouseEvent, layer: Layer): void; - onLayerToggleExpanded(event: MouseEvent, layer: Layer): void; - onLayerToggleVisibility(event: MouseEvent, layer: Layer): void; - onAddTimelineBlockClick(event: MouseEvent, layer: Layer, propertyName: string): void; - onConvertToClipPathClick(event: MouseEvent, layer: Layer): void; - onConvertToPathClick(event: MouseEvent, layer: Layer): void; - onFlattenGroupClick(event: MouseEvent, layer: Layer): void; -} - -interface LayerEvent { - readonly event: MouseEvent; - readonly layer: Layer; -} - -interface TimelineBlockEvent { - readonly event: MouseEvent; - readonly layer: Layer; - readonly propertyName: string; -} - -interface LayerModel { - readonly animation: Animation; - readonly isSelected: boolean; - readonly isHovered: boolean; - readonly isExpandable: boolean; - readonly isExpanded: boolean; - readonly isVisible: boolean; - readonly availablePropertyNames: ReadonlyArray; - readonly existingPropertyNames: ReadonlyArray; - readonly isActionMode: boolean; - readonly canBeConvertedToPath: boolean; - readonly canBeConvertedToClipPath: boolean; - readonly canBeFlattened: boolean; -} diff --git a/src/app/pages/editor/components/layertimeline/layertimeline.component.html b/src/app/pages/editor/components/layertimeline/layertimeline.component.html deleted file mode 100644 index 8fdc2c58..00000000 --- a/src/app/pages/editor/components/layertimeline/layertimeline.component.html +++ /dev/null @@ -1,252 +0,0 @@ - -
- - - -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - -
- -
- -
- - - -
-
-
-
- To get started, drag + drop an SVG file here -
-
-
- - -
-
-
-
-
- - {{ model.animation.name }} - - - {{ model.animation.duration }}ms - -
- -
- - -
- - - -
-
- - - -
-
-
-
-
-
\ No newline at end of file diff --git a/src/app/pages/editor/components/layertimeline/layertimeline.component.scss b/src/app/pages/editor/components/layertimeline/layertimeline.component.scss deleted file mode 100644 index becbc202..00000000 --- a/src/app/pages/editor/components/layertimeline/layertimeline.component.scss +++ /dev/null @@ -1,242 +0,0 @@ -.studio-layer-timeline { - height: 300px; - display: flex; - flex-direction: row; - flex-shrink: 0; - overflow: hidden; - z-index: 1; - position: relative; - $headerHeight: 40px; - $timelineAnimationPadding: 20px; - ul { - margin: 0; - padding: 0; - } - li { - list-style: none; - } - .slt-layers-list { - z-index: 2; - } - .slt-layer-container { - padding-top: 8px; - } - .slt-layer { - box-sizing: border-box; - line-height: 20px; - height: 20px; - } - .slt-layers { - width: 300px; - position: relative; - z-index: 3; - user-select: none; - flex: 0 0 auto; - mat-icon { - width: 16px; - height: 16px; - font-size: 16px; - } - .slt-layers-list-scroller { - position: relative; - overflow-x: hidden; - overflow-y: auto; - -ms-overflow-style: none; - &::-webkit-scrollbar { - display: none; - } - } - .slt-layers-list { - padding-left: 8px; - } - mat-menu { - display: flex; - } - .slt-header { - padding: 0 0 0 4px; - overflow: hidden; - .slt-layers-menu-group-button { - position: relative; - cursor: pointer; - border: 0; - height: $headerHeight; - font-size: 12px; - font-weight: 500; - line-height: $headerHeight; - padding: 0 8px 0 8px; - margin: 0; - outline: 0; - background-color: transparent; - &.is-disabled { - cursor: default; - } - } - button { - &[mat-icon-button] { - mat-icon { - line-height: 0px; - } - } - &[mat-icon-button][disabled] { - mat-icon { - line-height: 0px; - } - } - } - } - .slt-layers-list-drag-indicator { - position: absolute; - height: 2px; - left: 0; - right: 0; - margin-top: -1px; - pointer-events: none; - &::before { - position: absolute; - content: ''; - left: -4px; - top: -3px; - height: 8px; - width: 8px; - border-radius: 50%; - } - } - } - .slt-layers-empty { - padding: 32px; - text-align: center; - font-size: 14px; - line-height: 20px; - } - .slt-timeline { - overflow-x: auto; - overflow-y: hidden; - flex: 1; - user-select: none; - cursor: default; - display: flex; - flex-direction: row; - align-items: stretch; - .slt-timeline-animation-scroller { - position: relative; - overflow-x: hidden; - overflow-y: auto; - -ms-overflow-style: none; - &::-webkit-scrollbar { - display: none; - } - } - .slt-timeline-animation { - position: relative; - margin-left: 4px; - box-sizing: content-box; - width: 100px; - overflow: hidden; - flex: 0 0 auto; - opacity: 1; - &.is-disabled { - opacity: 0.5; - } - } - .slt-timeline-animation-rows { - padding-top: 8px; - padding-left: $timelineAnimationPadding; - padding-right: $timelineAnimationPadding; - z-index: 2; - } - .slt-timeline-grid { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; - pointer-events: none; - z-index: 1; - } - .slt-header { - display: flex; - flex-direction: column; - margin: 0; - .slt-timeline-animation-meta { - height: $headerHeight / 2 - 4px; - line-height: $headerHeight / 2 - 4px; - margin: 2px -4px; - padding: 0 4px; - border-radius: 2px; - cursor: pointer; - outline: 0; - align-self: flex-start; - display: flex; - flex-direction: row; - &.is-disabled { - cursor: default; - } - } - .slt-timeline-animation-name { - margin-right: 4px; - font-weight: 500; - } - .slt-timeline-header-grid { - position: absolute; - left: 0; - top: 50%; - width: 100%; - height: 50%; - z-index: 1; - cursor: pointer; - } - mat-icon { - width: 13px; - height: 13px; - font-size: 13px; - } - button { - line-height: 16px; - height: 16px; - width: 16px; - min-height: 16px; - padding-left: 1.5px; - padding-right: 1.5px; - padding-top: 1px; - padding-bottom: 1px; - margin-left: 4px; - &[mat-icon-button] { - mat-icon { - line-height: normal; - vertical-align: top; - } - } - } - } - } - .slt-header { - position: relative; - flex: 0 0 auto; - height: $headerHeight; - box-sizing: border-box; - width: 100%; - font-size: 12px; - line-height: $headerHeight; - padding: 0 16px; - z-index: 2; - } -} - -button { - &[mat-menu-item] { - input[type='file'] { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - opacity: 0; - cursor: pointer; - width: 0.1px; - height: 0.1px; - } - ::-webkit-file-upload-button { - cursor: pointer; - } - } -} diff --git a/src/app/pages/editor/components/layertimeline/layertimeline.component.ts b/src/app/pages/editor/components/layertimeline/layertimeline.component.ts deleted file mode 100644 index 566dd3ad..00000000 --- a/src/app/pages/editor/components/layertimeline/layertimeline.component.ts +++ /dev/null @@ -1,1173 +0,0 @@ -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - ElementRef, - OnInit, - QueryList, - ViewChild, - ViewChildren, -} from '@angular/core'; -import { DialogService } from 'app/pages/editor/components/dialogs'; -import { ProjectService } from 'app/pages/editor/components/project'; -import { ActionMode } from 'app/pages/editor/model/actionmode'; -import { - ClipPathLayer, - GroupLayer, - Layer, - LayerUtil, - PathLayer, - VectorLayer, -} from 'app/pages/editor/model/layers'; -import { Animation, AnimationBlock } from 'app/pages/editor/model/timeline'; -import { ModelUtil } from 'app/pages/editor/scripts/common'; -import { Dragger } from 'app/pages/editor/scripts/dragger'; -import { IntervalTree } from 'app/pages/editor/scripts/intervals'; -import { DestroyableMixin } from 'app/pages/editor/scripts/mixins'; -import { - ActionModeService, - FileExportService, - FileImportService, - LayerTimelineService, - PlaybackService, - ThemeService, -} from 'app/pages/editor/services'; -import { Shortcut, ShortcutService } from 'app/pages/editor/services/shortcut.service'; -import { Duration, SnackBarService } from 'app/pages/editor/services/snackbar.service'; -import { State, Store } from 'app/pages/editor/store'; -import { BatchAction } from 'app/pages/editor/store/batch/actions'; -import { getLayerTimelineState, isWorkspaceDirty } from 'app/pages/editor/store/common/selectors'; -import { SetSelectedLayers, SetVectorLayer } from 'app/pages/editor/store/layers/actions'; -import { getVectorLayer } from 'app/pages/editor/store/layers/selectors'; -import { ResetWorkspace } from 'app/pages/editor/store/reset/actions'; -import { getAnimation } from 'app/pages/editor/store/timeline/selectors'; -import { environment } from 'environments/environment'; -import * as $ from 'jquery'; -import * as _ from 'lodash'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { filter, first, map } from 'rxjs/operators'; - -import * as TimelineConsts from './constants'; -import { Callbacks as LayerListTreeCallbacks } from './layerlisttree.component'; -import { LayerTimelineGridDirective, ScrubEvent } from './layertimelinegrid.directive'; -import { Callbacks as TimelineAnimationRowCallbacks } from './timelineanimationrow.component'; - -const IS_DEV_BUILD = !environment.production; - -// Distance in pixels from a snap point before snapping to the point. -const SNAP_PIXELS = 10; -const LAYER_INDENT_PIXELS = 20; -const MIN_BLOCK_DURATION = 10; -const MAX_ZOOM = 10; -const MIN_ZOOM = 0.01; -const DEFAULT_HORIZ_ZOOM = 2; // 1ms = 2px. - -enum MouseActions { - // We are dragging a block to a different location on the timeline. - Moving = 1, - // Scales all selected blocks w/o altering their initial positions. - ScalingUniformStart, - ScalingUniformEnd, - // Scales all blocks and also translates their initial positions. - ScalingTogetherStart, - ScalingTogetherEnd, -} - -declare const ga: Function; - -@Component({ - selector: 'app-layertimeline', - templateUrl: './layertimeline.component.html', - styleUrls: ['./layertimeline.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class LayerTimelineComponent extends DestroyableMixin() - implements OnInit, AfterViewInit, TimelineAnimationRowCallbacks, LayerListTreeCallbacks { - @ViewChild('timeline') private timelineRef: ElementRef; - private $timeline: JQuery; - - @ViewChild('timelineAnimation') private timelineAnimationRef: ElementRef; - @ViewChildren(LayerTimelineGridDirective) - timelineDirectives: QueryList; - - private readonly dragIndicatorSubject = new BehaviorSubject({ - isVisible: false, - left: 0, - top: 0, - }); - dragIndicatorObservable = this.dragIndicatorSubject.asObservable(); - private readonly horizZoomSubject = new BehaviorSubject(DEFAULT_HORIZ_ZOOM); - horizZoomObservable = this.horizZoomSubject.asObservable(); - private currentTime_ = 0; - - private shouldSuppressClick = false; - private shouldSuppressRebuildSnapTimes = false; - private snapTimes: Map; - - private animation: Animation; - private vectorLayer: VectorLayer; - private selectedBlockIds: ReadonlySet; - - layerTimelineModel$: Observable; - - // Mouse wheel zoom variables. - private $zoomStartActiveAnimation: JQuery; - private targetHorizZoom: number; - private performZoomRAF: number = undefined; - private endZoomTimeout: number = undefined; - private zoomStartTimeCursorPos: number; - - constructor( - private readonly fileImportService: FileImportService, - private readonly fileExportService: FileExportService, - private readonly snackBarService: SnackBarService, - private readonly playbackService: PlaybackService, - private readonly store: Store, - private readonly dialogService: DialogService, - private readonly projectService: ProjectService, - private readonly actionModeService: ActionModeService, - readonly shortcutService: ShortcutService, - private readonly layerTimelineService: LayerTimelineService, - readonly themeService: ThemeService, - ) { - super(); - } - - ngOnInit() { - let currActionMode: ActionMode; - this.layerTimelineModel$ = this.store.select(getLayerTimelineState).pipe( - map( - ({ - animation, - vectorLayer, - isAnimationSelected, - selectedBlockIds, - isBeingReset, - isActionMode, - actionMode, - singleSelectedPathBlock, - }) => { - this.animation = animation; - this.rebuildSnapTimes(); - this.vectorLayer = vectorLayer; - this.selectedBlockIds = selectedBlockIds; - if (isBeingReset) { - // TODO: store the 'zoom' info in the store to avoid using this isBeingReset flag - this.autoZoomToAnimation(); - } - if (currActionMode === ActionMode.None && actionMode === ActionMode.Selection) { - // Move the current time to the beginning of the selected block when - // entering action mode. - this.playbackService.setCurrentTime(singleSelectedPathBlock.startTime); - } - currActionMode = actionMode; - return { - animation, - vectorLayer, - isAnimationSelected, - isActionMode, - }; - }, - ), - ); - this.registerSubscription( - this.shortcutService.asObservable().subscribe(shortcut => { - if (shortcut === Shortcut.ZoomToFit) { - this.autoZoomToAnimation(); - } - }), - ); - } - - ngAfterViewInit() { - this.$timeline = $(this.timelineRef.nativeElement); - this.registerSubscription( - this.playbackService.asObservable().subscribe(event => { - // TODO: make this reactive/avoid storing current time locally - this.currentTime = event.currentTime; - }), - ); - setTimeout(() => this.autoZoomToAnimation()); - } - - private get horizZoom() { - return this.horizZoomSubject.getValue(); - } - - private set horizZoom(horizZoom: number) { - this.horizZoomSubject.next(horizZoom); - } - - private get currentTime() { - return this.currentTime_; - } - - private set currentTime(currentTime: number) { - this.currentTime_ = currentTime; - this.timelineDirectives.forEach(dir => (dir.currentTime = currentTime)); - } - - // Called from the LayerTimelineComponent template. - onNewWorkspaceClick() { - const resetWorkspaceFn = () => { - ga('send', 'event', 'File', 'New'); - this.store.dispatch(new ResetWorkspace()); - }; - this.store - .select(isWorkspaceDirty) - .pipe(first()) - .subscribe(isDirty => { - if (isDirty && !IS_DEV_BUILD) { - this.dialogService - .confirm('Start over?', `You'll lose any unsaved changes.`) - .pipe(filter(result => result)) - .subscribe(resetWorkspaceFn); - } else { - resetWorkspaceFn(); - } - }); - } - - // Called from the LayerTimelineComponent template. - onSaveToFileClick() { - ga('send', 'event', 'File', 'Save'); - this.fileExportService.exportJSON(); - } - - // Called from the LayerTimelineComponent template. - onLoadDemoClick() { - ga('send', 'event', 'File', 'Demo'); - this.dialogService - .pickDemo() - .pipe(filter(demoInfo => !!demoInfo)) - .subscribe(selectedDemoInfo => { - ga('send', 'event', 'Demos', 'Demo selected', selectedDemoInfo.title); - - this.projectService - .getProject(`demos/${selectedDemoInfo.id}.shapeshifter`) - .then(({ vectorLayer, animation, hiddenLayerIds }) => { - this.store.dispatch(new ResetWorkspace(vectorLayer, animation, hiddenLayerIds)); - }) - .catch(error => { - const msg = - 'serviceWorker' in navigator && navigator.serviceWorker.controller - ? 'Demo not available offline' - : `Couldn't fetch demo`; - this.snackBarService.show(msg, 'Dismiss', Duration.Long); - return Promise.reject(error.message || error); - }); - }); - } - - // Called from the LayerTimelineComponent template. - onExportSvgClick() { - ga('send', 'event', 'Export', 'SVG'); - this.fileExportService.exportSvg(); - } - - // Called from the LayerTimelineComponent template. - onExportVectorDrawableClick() { - ga('send', 'event', 'Export', 'Vector Drawable'); - this.fileExportService.exportVectorDrawable(); - } - - // Called from the LayerTimelineComponent template. - onExportAnimatedVectorDrawableClick() { - ga('send', 'event', 'Export', 'Animated Vector Drawable'); - this.fileExportService.exportAnimatedVectorDrawable(); - } - - // Called from the LayerTimelineComponent template. - onExportSvgSpritesheetClick() { - ga('send', 'event', 'Export', 'SVG Spritesheet'); - this.fileExportService.exportSvgSpritesheet(); - } - - // Called from the LayerTimelineComponent template. - onExportCssKeyframesClick() { - // TODO: implement this feature - ga('send', 'event', 'Export', 'CSS Keyframes'); - this.fileExportService.exportCssKeyframes(); - } - - // Called from the LayerTimelineComponent template. - onAnimationHeaderTextClick(event: MouseEvent) { - // Stop propagation to ensure that animationTimelineClick() isn't called. - event.stopPropagation(); - if (!this.actionModeService.isActionMode()) { - const isSelected = !ShortcutService.isOsDependentModifierKey(event) && !event.shiftKey; - this.layerTimelineService.selectAnimation(isSelected); - } - } - - // Called from the LayerTimelineComponent template. - onTimelineHeaderScrub(event: ScrubEvent) { - let time = event.time; - if (!event.disableSnap) { - time = this.snapTime(time, false); - } - this.currentTime = time; - this.playbackService.setCurrentTime(time); - } - - // Called from the LayerTimelineComponent template. - onAddPathLayerClick() { - this.store - .select(getVectorLayer) - .pipe(first()) - .subscribe(vl => { - const layer = new PathLayer({ - name: LayerUtil.getUniqueLayerName([vl], 'path'), - children: [], - pathData: undefined, - }); - this.layerTimelineService.addLayer(layer); - }); - } - - // Called from the LayerTimelineComponent template. - onAddClipPathLayerClick() { - this.store - .select(getVectorLayer) - .pipe(first()) - .subscribe(vl => { - const layer = new ClipPathLayer({ - name: LayerUtil.getUniqueLayerName([vl], 'mask'), - children: [], - pathData: undefined, - }); - this.layerTimelineService.addLayer(layer); - }); - } - - // Called from the LayerTimelineComponent template. - onAddGroupLayerClick() { - this.store - .select(getVectorLayer) - .pipe(first()) - .subscribe(vl => { - const name = LayerUtil.getUniqueLayerName([vl], 'group'); - const layer = new GroupLayer({ name, children: [] }); - this.layerTimelineService.addLayer(layer); - }); - } - - // @Override TimelineAnimationRowCallbacks - onTimelineBlockMouseDown(mouseDownEvent: MouseEvent, dragBlock: AnimationBlock) { - const animation = this.animation; - // TODO: this JQuery 'class' stuff may not work with view encapsulation enabled - const $target = $(mouseDownEvent.target); - - // Some geometry and hit-testing basics. - const animRect = $(mouseDownEvent.target) - .parents('.slt-property') - .get(0) - .getBoundingClientRect(); - const xToTimeFn = (x: number) => ((x - animRect.left) / animRect.width) * animation.duration; - const downTime = xToTimeFn(mouseDownEvent.clientX); - - // Determine the action based on where the user clicked and the modifier keys. - const metaKey = ShortcutService.isOsDependentModifierKey(mouseDownEvent); - let action = MouseActions.Moving; - if ($target.hasClass('slt-timeline-block-edge-end')) { - action = - mouseDownEvent.shiftKey || metaKey - ? MouseActions.ScalingTogetherEnd - : MouseActions.ScalingUniformEnd; - } else if ($target.hasClass('slt-timeline-block-edge-start')) { - action = - mouseDownEvent.shiftKey || metaKey - ? MouseActions.ScalingTogetherStart - : MouseActions.ScalingUniformStart; - } - - // Start up a cache of info for each selected block, calculating the left and right - // bounds for each selected block, based on adjacent non-dragging blocks. - const blocksByPropertyByLayer = ModelUtil.getOrderedBlocksByPropertyByLayer(animation); - - // Either drag all selected blocks or just the mousedown block. - const draggingBlocks = this.selectedBlockIds.has(dragBlock.id) - ? animation.blocks.filter(b => this.selectedBlockIds.has(b.id)) - : [dragBlock]; - const stagnantBlocks = animation.blocks.filter(block => { - return ( - draggingBlocks.every(b => block.id !== b.id) && - draggingBlocks.some(({ layerId, propertyName }) => { - return block.layerId === layerId && block.propertyName === propertyName; - }) - ); - }); - - interface IntervalInfo { - readonly blockId: string; - readonly layerId: string; - readonly propertyName: string; - readonly startTime: number; - readonly endTime: number; - } - - const intervalTree = new IntervalTree(); - stagnantBlocks.forEach(b => { - const { id, layerId, propertyName, startTime, endTime } = b; - intervalTree.insert(Math.min(startTime, animation.duration), Math.max(0, endTime), { - blockId: id, - layerId, - propertyName, - startTime, - endTime, - }); - }); - - interface BlockInfo { - readonly block: AnimationBlock; - readonly downStartTime: number; - readonly downEndTime: number; - readonly startBound?: number; - readonly endBound?: number; - newStartTime?: number; - newEndTime?: number; - } - - const blockInfos: BlockInfo[] = draggingBlocks.map(block => { - const blockNeighbors = blocksByPropertyByLayer[block.layerId][block.propertyName]; - const indexIntoNeighbors = _.findIndex(blockNeighbors, b => block.id === b.id); - - // By default the block is only bound by the animation duration. - let startBound = 0; - let endBound = animation.duration; - - // For each block find the left-most non-selected block and use that as - // the start bound. - if (indexIntoNeighbors > 0) { - for (let i = indexIntoNeighbors - 1; i >= 0; i--) { - const neighbor = blockNeighbors[i]; - if (!draggingBlocks.includes(neighbor) || action === MouseActions.ScalingUniformStart) { - // Only be bound by neighbors not being dragged - // except when uniformly changing just start time. - startBound = neighbor.endTime; - break; - } - } - } - - // For each block find the right-most non-selected block and use that as - // the end bound. - if (indexIntoNeighbors < blockNeighbors.length - 1) { - for (let i = indexIntoNeighbors + 1; i < blockNeighbors.length; i++) { - const neighbor = blockNeighbors[i]; - if (!draggingBlocks.includes(neighbor) || action === MouseActions.ScalingUniformEnd) { - // Only be bound by neighbors not being dragged - // except when uniformly changing just end time. - endBound = neighbor.startTime; - break; - } - } - } - - return { - block, - startBound, - endBound, - downStartTime: block.startTime, - downEndTime: block.endTime, - }; - }); - - const dragBlockDownStartTime = dragBlock.startTime; - const dragBlockDownEndTime = dragBlock.endTime; - - let minStartTime: number; - let maxEndTime: number; - if ( - action === MouseActions.ScalingTogetherStart || - action === MouseActions.ScalingTogetherEnd - ) { - minStartTime = blockInfos.reduce((t, info) => Math.min(t, info.block.startTime), Infinity); - maxEndTime = blockInfos.reduce((t, info) => Math.max(t, info.block.endTime), 0); - // Avoid divide by zero. - maxEndTime = Math.max(maxEndTime, minStartTime + MIN_BLOCK_DURATION); - } - - const isOverlappingBlockFn = (info: BlockInfo, low: number, high: number) => { - const { layerId, propertyName } = info.block; - return intervalTree.intersectsWith( - low, - high, - d => d.layerId === layerId && d.propertyName === propertyName, - ); - }; - - // tslint:disable-next-line: no-unused-expression - new Dragger({ - direction: 'horizontal', - downX: mouseDownEvent.clientX, - downY: mouseDownEvent.clientY, - draggingCursor: action === MouseActions.Moving ? 'grabbing' : 'ew-resize', - onBeginDragFn: () => { - this.shouldSuppressClick = true; - this.shouldSuppressRebuildSnapTimes = true; - }, - onDropFn: () => - setTimeout(() => { - this.shouldSuppressClick = false; - this.shouldSuppressRebuildSnapTimes = false; - this.rebuildSnapTimes(); - }, 0), - onDragFn: event => { - // Calculate the 'time delta' (the number of milliseconds the user has moved - // since the gesture began). - let timeDelta = Math.round(xToTimeFn(event.clientX) - downTime); - const allowSnap = !event.shiftKey && !ShortcutService.isOsDependentModifierKey(event); - const replacementBlocks: AnimationBlock[] = []; - switch (action) { - case MouseActions.Moving: { - blockInfos.forEach(info => { - // Snap time delta. - if (allowSnap && info.block.id === dragBlock.id) { - const newStartTime = info.downStartTime + timeDelta; - const newStartTimeSnapDelta = this.snapTime(newStartTime) - newStartTime; - const newEndTime = info.downEndTime + timeDelta; - const newEndTimeSnapDelta = this.snapTime(newEndTime) - newEndTime; - if (newStartTimeSnapDelta) { - if (newEndTimeSnapDelta) { - timeDelta += Math.min(newStartTimeSnapDelta, newEndTimeSnapDelta); - } else { - timeDelta += newStartTimeSnapDelta; - } - } else if (newEndTimeSnapDelta) { - timeDelta += newEndTimeSnapDelta; - } - } - // Clamp time delta to ensure it remains within the duration's bounds. - const min = -info.downStartTime; - const max = animation.duration - info.downEndTime; - timeDelta = _.clamp(timeDelta, min, max); - }); - - const deltas = _(blockInfos) - .filter(info => { - // For each block, check if it overlaps with any of the stagnant blocks. - const low = info.downStartTime + timeDelta; - const end = info.downEndTime + timeDelta; - return isOverlappingBlockFn(info, low, end); - }) - .flatMap(info => { - const { - block: { id, layerId, propertyName }, - } = info; - const neighbors = blocksByPropertyByLayer[layerId][propertyName].filter( - ngh => id !== ngh.id, - ); - return _.flatMap(neighbors, ngh => { - return [ngh.startTime - info.downEndTime, ngh.endTime - info.downStartTime]; - }); - }) - .sort((a, b) => Math.abs(a - timeDelta) - Math.abs(b - timeDelta)) - .value(); - - const deltaIndex = _.findIndex(deltas, delta => { - return blockInfos.every(info => { - const low = info.downStartTime + delta; - const high = info.downEndTime + delta; - if (low < 0 || high > animation.duration) { - return false; - } - return !isOverlappingBlockFn(info, low, high); - }); - }); - if (deltaIndex >= 0) { - timeDelta = deltas[deltaIndex]; - } - - blockInfos.forEach(info => { - const blockDuration = info.block.endTime - info.block.startTime; - const block = info.block.clone(); - block.startTime = info.downStartTime + timeDelta; - block.endTime = block.startTime + blockDuration; - replacementBlocks.push(block); - }); - break; - } - case MouseActions.ScalingUniformStart: { - blockInfos.forEach(info => { - // Snap time delta. - if (allowSnap && info.block.id === dragBlock.id) { - const newStartTime = info.downStartTime + timeDelta; - const newStartTimeSnapDelta = this.snapTime(newStartTime) - newStartTime; - if (newStartTimeSnapDelta) { - timeDelta += newStartTimeSnapDelta; - } - } - // Clamp time delta. - const min = info.startBound - info.downStartTime; - const max = info.block.endTime - MIN_BLOCK_DURATION - info.downStartTime; - timeDelta = _.clamp(timeDelta, min, max); - }); - blockInfos.forEach(info => { - const block = info.block.clone(); - block.startTime = info.downStartTime + timeDelta; - replacementBlocks.push(block); - }); - break; - } - case MouseActions.ScalingUniformEnd: { - blockInfos.forEach(info => { - // Snap time delta. - if (allowSnap && info.block === dragBlock) { - const newEndTime = info.downEndTime + timeDelta; - const newEndTimeSnapDelta = this.snapTime(newEndTime) - newEndTime; - if (newEndTimeSnapDelta) { - timeDelta += newEndTimeSnapDelta; - } - } - // Clamp time delta. - const min = info.block.startTime + MIN_BLOCK_DURATION - info.downEndTime; - const max = info.endBound - info.downEndTime; - timeDelta = _.clamp(timeDelta, min, max); - }); - blockInfos.forEach(info => { - const block = info.block.clone(); - block.endTime = info.downEndTime + timeDelta; - replacementBlocks.push(block); - }); - break; - } - case MouseActions.ScalingTogetherStart: { - let scale = - (dragBlockDownStartTime + timeDelta - maxEndTime) / - (dragBlockDownStartTime - maxEndTime); - scale = Math.min(scale, maxEndTime / (maxEndTime - minStartTime)); - let cancel = false; - blockInfos.forEach(info => { - info.newStartTime = maxEndTime - (maxEndTime - info.downStartTime) * scale; - info.newEndTime = Math.max( - maxEndTime - (maxEndTime - info.downEndTime) * scale, - info.newStartTime + MIN_BLOCK_DURATION, - ); - if (info.newStartTime < info.startBound || info.newEndTime > info.endBound) { - cancel = true; - } - }); - if (!cancel) { - blockInfos.forEach(info => { - const block = info.block.clone(); - block.startTime = info.newStartTime; - block.endTime = info.newEndTime; - replacementBlocks.push(block); - }); - } - break; - } - case MouseActions.ScalingTogetherEnd: { - let scale = - (dragBlockDownEndTime + timeDelta - minStartTime) / - (dragBlockDownEndTime - minStartTime); - scale = Math.min( - scale, - (animation.duration - minStartTime) / (maxEndTime - minStartTime), - ); - let cancel = false; - blockInfos.forEach(info => { - info.newStartTime = minStartTime + (info.downStartTime - minStartTime) * scale; - info.newEndTime = Math.max( - minStartTime + (info.downEndTime - minStartTime) * scale, - info.newStartTime + MIN_BLOCK_DURATION, - ); - if (info.newStartTime < info.startBound || info.newEndTime > info.endBound) { - cancel = true; - } - }); - if (!cancel) { - blockInfos.forEach(info => { - const block = info.block.clone(); - block.startTime = info.newStartTime; - block.endTime = info.newEndTime; - replacementBlocks.push(block); - }); - } - break; - } - } - this.store - .select(getAnimation) - .pipe(first()) - .subscribe(anim => { - const blocks = replacementBlocks.filter(replacementBlock => { - // Note that existingBlock may not be found if changes were made to the animation - // (i.e. a block was deleted during a drag). - const existingBlock = _.find(anim.blocks, b => replacementBlock.id === b.id); - return ( - existingBlock && - (replacementBlock.startTime !== existingBlock.startTime || - replacementBlock.endTime !== existingBlock.endTime) - ); - }); - this.layerTimelineService.updateBlocks(blocks); - }); - }, - }); - } - - /** - * Builds a cache of snap times for all available animations. - */ - private rebuildSnapTimes() { - if (this.shouldSuppressRebuildSnapTimes) { - return; - } - this.snapTimes = new Map(); - const snapTimesSet = new Set(); - snapTimesSet.add(0); - snapTimesSet.add(this.animation.duration); - this.animation.blocks.forEach(block => { - snapTimesSet.add(block.startTime); - snapTimesSet.add(block.endTime); - }); - this.snapTimes.set(this.animation.id, Array.from(snapTimesSet)); - } - - /** - * Returns a new time, possibly snapped to animation boundaries - */ - private snapTime(time: number, includeActiveTime = true) { - const animation = this.animation; - const snapTimes = this.snapTimes.get(animation.id); - const snapDelta = SNAP_PIXELS / this.horizZoom; - const reducerFn = (best: number, snapTime: number) => { - const dist = Math.abs(time - snapTime); - return dist < snapDelta && dist < Math.abs(time - best) ? snapTime : best; - }; - let bestSnapTime = snapTimes.reduce(reducerFn, Infinity); - if (includeActiveTime) { - bestSnapTime = reducerFn(bestSnapTime, this.currentTime); - } - return isFinite(bestSnapTime) ? bestSnapTime : time; - } - - // @Override TimelineAnimationRowCallbacks - onTimelineBlockClick(event: MouseEvent, block: AnimationBlock) { - const clearExisting = !ShortcutService.isOsDependentModifierKey(event) && !event.shiftKey; - this.layerTimelineService.selectBlock(block.id, clearExisting); - } - - // @Override TimelineAnimationRowCallbacks - onTimelineBlockDoubleClick(event: MouseEvent, block: AnimationBlock) { - this.playbackService.setCurrentTime(block.startTime); - } - - // @Override LayerListTreeComponentCallbacks - onAddTimelineBlockClick(event: MouseEvent, layer: Layer, propertyName: string) { - const clonedValue = layer.inspectableProperties - .get(propertyName) - .cloneValue((layer as any)[propertyName]); - this.layerTimelineService.addBlocks([ - { - layerId: layer.id, - propertyName, - fromValue: clonedValue, - toValue: clonedValue, - currentTime: this.currentTime, - }, - ]); - } - - // @Override LayerListTreeComponentCallbacks - onConvertToClipPathClick(event: MouseEvent, layer: Layer) { - const clipPathLayer = new ClipPathLayer(layer as PathLayer); - clipPathLayer.id = _.uniqueId(); - this.layerTimelineService.swapLayers(layer.id, clipPathLayer); - } - - // @Override LayerListTreeComponentCallbacks - onConvertToPathClick(event: MouseEvent, layer: Layer) { - const pathLayer = new PathLayer(layer as ClipPathLayer); - pathLayer.id = _.uniqueId(); - this.layerTimelineService.swapLayers(layer.id, pathLayer); - } - - // @Override LayerListTreeComponentCallbacks - onFlattenGroupClick(event: MouseEvent, layer: Layer) { - this.layerTimelineService.flattenGroupLayer(layer.id); - } - - // @Override LayerListTreeComponentCallbacks - onLayerClick(event: MouseEvent, clickedLayer: Layer) { - const isMeta = ShortcutService.isOsDependentModifierKey(event); - const isShift = event.shiftKey; - if (!isMeta && !isShift) { - // Clear the existing selections. - this.layerTimelineService.selectLayer(clickedLayer.id, true); - return; - } - - if (isMeta && !isShift) { - // Add the single layer to the existing selections, toggling the - // layer if it is already selected. - this.layerTimelineService.selectLayer(clickedLayer.id, false); - return; - } - - if (isMeta && isShift) { - // Add the single layer to the existing selections. - const layerIds = this.layerTimelineService.getSelectedLayerIds(); - layerIds.add(clickedLayer.id); - this.layerTimelineService.setSelectedLayers(layerIds); - return; - } - - // Batch add layers to the existing selections. - const { vectorLayer } = this; - const topDownSortedLayers = LayerUtil.runPreorderTraversal(vectorLayer); - const clickedLayerIndex = _.findIndex(topDownSortedLayers, l => l.id === clickedLayer.id); - const selectedLayerIds = this.layerTimelineService.getSelectedLayerIds(); - // TODO: re-implement this behavior to match the behavior of Sketch - // TODO will need to store most recently selected layer ID in order to implement this behavior - const { startIndex, endIndex } = (function() { - // Find the first selected layer before clickedLayerIndex. - const beforeLayerIndex = _.findLastIndex( - topDownSortedLayers, - l => selectedLayerIds.has(l.id), - clickedLayerIndex, - ); - if (beforeLayerIndex >= 0) { - // Batch select [beforeLayerIndex, clickedLayerIndex]. - return { startIndex: beforeLayerIndex, endIndex: clickedLayerIndex }; - } - // Find the first selected layer after clickedLayerIndex. - const afterLayerIndex = _.findIndex( - topDownSortedLayers, - l => selectedLayerIds.has(l.id), - clickedLayerIndex, - ); - if (afterLayerIndex >= 0) { - // Batch select [clickedLayerIndex, afterLayerIndex]. - return { startIndex: clickedLayerIndex, endIndex: afterLayerIndex }; - } - // Batch select [0, clickedLayerIndex]. - return { startIndex: 0, endIndex: clickedLayerIndex }; - })(); - for (let i = startIndex; i <= endIndex; i++) { - selectedLayerIds.add(topDownSortedLayers[i].id); - } - this.layerTimelineService.setSelectedLayers(selectedLayerIds); - } - - // @Override LayerListTreeComponentCallbacks - onLayerToggleExpanded(event: MouseEvent, layer: Layer) { - const recursive = ShortcutService.isOsDependentModifierKey(event) || event.shiftKey; - this.layerTimelineService.toggleExpandedLayer(layer.id, recursive); - } - - // @Override LayerListTreeComponentCallbacks - onLayerToggleVisibility(event: MouseEvent, layer: Layer) { - this.layerTimelineService.toggleVisibleLayer(layer.id); - } - - // @Override LayerListTreeComponentCallbacks - onLayerMouseDown(mouseDownEvent: MouseEvent, mouseDownDragLayer: Layer) { - const $layersList = $(mouseDownEvent.target).parents('.slt-layers-list'); - const $scroller = $(mouseDownEvent.target).parents('.slt-layers-list-scroller'); - - interface LayerInfo { - layer: Layer; - element: Element; - localRect: ClientRect; - moveIntoEmptyLayerGroup?: boolean; - } - - let orderedLayerInfos: LayerInfo[] = []; - let scrollerRect: ClientRect; - let targetLayerInfo: LayerInfo; - let targetEdge: string; - - const dragLayers: ReadonlyArray = (function(lts: LayerTimelineService) { - const selectedLayerIds = lts.getSelectedLayerIds(); - // Don't drag any other selected layers if the drag layer isn't selected itself. - // At the end of the drag, we will select the drag layer and deselect the others. - const dragLayerIdSet = selectedLayerIds.has(mouseDownDragLayer.id) - ? selectedLayerIds - : new Set([mouseDownDragLayer.id]); - const topDownSortedLayers = LayerUtil.runPreorderTraversal(lts.getVectorLayer()); - return topDownSortedLayers.filter(l => dragLayerIdSet.has(l.id)); - })(this.layerTimelineService); - - // tslint:disable-next-line: no-unused-expression - new Dragger({ - direction: 'both', - downX: mouseDownEvent.clientX, - downY: mouseDownEvent.clientY, - - onBeginDragFn: () => { - this.shouldSuppressClick = true; - - // Build up a list of all layers ordered by Y position. - orderedLayerInfos = []; - scrollerRect = $scroller.get(0).getBoundingClientRect(); - const scrollTop = $scroller.scrollTop(); - $layersList.find('.slt-layer-container').each((__, element) => { - // toString() is necessary because JQuery converts the ID into a number. - const layerId: string = ($(element).data('layer-id') || '').toString(); - if (!layerId) { - // The root layer doesn't have an ID set. - return; - } - - let rect = element.getBoundingClientRect(); - rect = { - left: rect.left, - top: rect.top + scrollTop - scrollerRect.top, - bottom: rect.bottom + scrollTop - scrollerRect.top, - height: rect.height, - right: rect.right, - width: rect.width, - }; - - const layer = this.vectorLayer.findLayerById(layerId); - orderedLayerInfos.push({ layer, element, localRect: rect }); - - // Add a fake target for empty groups. - if (layer instanceof GroupLayer && !layer.children.length) { - const left = rect.left + LAYER_INDENT_PIXELS; - const top = rect.bottom; - rect = { ...rect, ...{ left, top } }; - orderedLayerInfos.push({ - layer, - element, - localRect: rect, - moveIntoEmptyLayerGroup: true, - }); - } - }); - - orderedLayerInfos.sort((a, b) => a.localRect.top - b.localRect.top); - this.updateDragIndicator({ isVisible: true, left: 0, top: 0 }); - }, - - onDragFn: event => { - const localEventY = event.clientY - scrollerRect.top + $scroller.scrollTop(); - // Find the target layer and edge (top or bottom). - targetLayerInfo = undefined; - let minDistance = Infinity; - let minDistanceIndent = Infinity; // Tie break to most indented layer. - for (const layerInfo of orderedLayerInfos) { - // Skip if mouse to the left of this layer. - if (event.clientX < layerInfo.localRect.left) { - continue; - } - - for (const edge of ['top', 'bottom']) { - // Test distance to top edge. - const distance = Math.abs(localEventY - layerInfo.localRect[edge as 'top' | 'bottom']); - const indent = layerInfo.localRect.left; - if (distance <= minDistance) { - if (distance !== minDistance || indent > minDistanceIndent) { - minDistance = distance; - minDistanceIndent = indent; - targetLayerInfo = layerInfo; - targetEdge = edge; - } - } - } - } - - // Disallow dragging a layer into itself or its children. - if (targetLayerInfo) { - let { layer } = targetLayerInfo; - while (layer) { - if (_.find(dragLayers, l => l.id === layer.id)) { - targetLayerInfo = undefined; - break; - } - layer = LayerUtil.findParent(this.vectorLayer, layer.id); - } - } - - if (targetLayerInfo && targetEdge === 'bottom') { - const nextSibling = LayerUtil.findNextSibling(this.vectorLayer, targetLayerInfo.layer.id); - if (nextSibling && nextSibling.id === mouseDownDragLayer.id) { - targetLayerInfo = undefined; - } - } - - const dragIndicatorInfo: DragIndicatorInfo = { isVisible: !!targetLayerInfo }; - if (targetLayerInfo) { - dragIndicatorInfo.left = targetLayerInfo.localRect.left; - dragIndicatorInfo.top = targetLayerInfo.localRect[targetEdge as 'top' | 'bottom']; - } - this.updateDragIndicator(dragIndicatorInfo); - }, - - onDropFn: () => { - this.updateDragIndicator({ isVisible: false }); - setTimeout(() => (this.shouldSuppressClick = false)); - - if (!targetLayerInfo) { - return; - } - - const dragLayerIds: ReadonlyArray = dragLayers.map(l => l.id); - - const addDragLayersFn = ( - vl: VectorLayer, - parent: Layer, - startingIndex = parent.children.length, - ) => { - const layersToAdd = dragLayers.map(l => { - const otherDragLayerIds = dragLayerIds.filter(id => id !== l.id); - return LayerUtil.removeLayers(l, ...otherDragLayerIds); - }); - return LayerUtil.addLayers(vl, parent.id, startingIndex, ...layersToAdd); - }; - - const removeDragLayersFn = (vl: VectorLayer) => LayerUtil.removeLayers(vl, ...dragLayerIds); - - const initialVl = this.vectorLayer; - let replacementVl: VectorLayer; - - if (targetLayerInfo.moveIntoEmptyLayerGroup) { - // Moving into an empty layer group. - replacementVl = addDragLayersFn(removeDragLayersFn(initialVl), targetLayerInfo.layer); - } else if (LayerUtil.findParent(initialVl, targetLayerInfo.layer.id)) { - // Moving next to another layer. - const tempVl = removeDragLayersFn(initialVl); - const parent = LayerUtil.findParent(tempVl, targetLayerInfo.layer.id); - const index = _.findIndex(parent.children, l => l.id === targetLayerInfo.layer.id); - if (index >= 0) { - replacementVl = addDragLayersFn(tempVl, parent, index + (targetEdge === 'top' ? 0 : 1)); - } - } - - if (replacementVl) { - this.store.dispatch( - new BatchAction( - new SetVectorLayer(replacementVl), - new SetSelectedLayers(new Set(dragLayerIds)), - ), - ); - } - }, - }); - } - - private updateDragIndicator(info: DragIndicatorInfo) { - const curr = this.dragIndicatorSubject.getValue(); - this.dragIndicatorSubject.next({ ...curr, ...info }); - } - - /** - * Handles ctrl + mouse wheel event for zooming into and out of the timeline. - */ - onWheelEvent(event: WheelEvent) { - const startZoomFn = () => { - this.$zoomStartActiveAnimation = $(this.timelineAnimationRef.nativeElement); - this.zoomStartTimeCursorPos = - this.$zoomStartActiveAnimation.position().left + - this.currentTime * this.horizZoom + - TimelineConsts.TIMELINE_ANIMATION_PADDING; - }; - - const performZoomFn = () => { - this.horizZoom = this.targetHorizZoom; - - // Set the scroll offset such that the time cursor remains at zoomStartTimeCursorPos - if (this.$zoomStartActiveAnimation) { - const newScrollLeft = - this.$zoomStartActiveAnimation.position().left + - this.$timeline.scrollLeft() + - this.currentTime * this.horizZoom + - TimelineConsts.TIMELINE_ANIMATION_PADDING - - this.zoomStartTimeCursorPos; - this.$timeline.scrollLeft(newScrollLeft); - } - }; - - const endZoomFn = () => { - this.zoomStartTimeCursorPos = 0; - this.$zoomStartActiveAnimation = undefined; - this.endZoomTimeout = undefined; - this.targetHorizZoom = 0; - }; - - // chrome+mac trackpad pinch-zoom = ctrlKey - if (ShortcutService.isOsDependentModifierKey(event) || event.ctrlKey) { - if (!this.targetHorizZoom) { - // Multiple changes can happen to targetHorizZoom before the - // actual zoom level is updated (see performZoom_). - this.targetHorizZoom = this.horizZoom; - } - - event.preventDefault(); - this.targetHorizZoom *= 1.01 ** -event.deltaY; - this.targetHorizZoom = _.clamp(this.targetHorizZoom, MIN_ZOOM, MAX_ZOOM); - if (this.targetHorizZoom !== this.horizZoom) { - // Zoom has changed. - if (this.performZoomRAF) { - window.cancelAnimationFrame(this.performZoomRAF); - } - this.performZoomRAF = window.requestAnimationFrame(() => performZoomFn()); - if (this.endZoomTimeout) { - window.clearTimeout(this.endZoomTimeout); - } else { - startZoomFn(); - } - this.endZoomTimeout = window.setTimeout(() => endZoomFn(), 100); - } - return false; - } - return undefined; - } - - // Called from the LayerTimelineComponent template. - onZoomToFitClick(event: MouseEvent) { - event.stopPropagation(); - this.autoZoomToAnimation(); - } - - /** - * Zooms the timeline to fit the first animation. - */ - private autoZoomToAnimation() { - // Shave off 48 pixels for safety. - this.horizZoom = (this.$timeline.width() - 48) / this.animation.duration; - } - - // Proxies a button click to the tag that opens the file picker. - // We clear the element's value to make it possible to import the same file - // more than once. - onLaunchFilePickerClick(event: MouseEvent, sourceElementId: string) { - $(`#${sourceElementId}`) - .val('') - .trigger('click'); - } - - // Called from the LayerTimelineComponent template. - onImportedFilesPicked(event: MouseEvent, fileList: FileList) { - // TODO: determine if calling stopPropogation() is needed? - event.stopPropagation(); - this.fileImportService.import(fileList); - } - - onTopSplitterChanged() { - if (this.timelineDirectives) { - this.timelineDirectives.forEach(d => d.redraw()); - } - } - - // Used by *ngFor loop. - trackLayerFn(index: number, layer: Layer) { - return layer.id; - } -} - -interface LayerTimelineModel { - readonly animation: Animation; - readonly vectorLayer: VectorLayer; - readonly isAnimationSelected: boolean; - readonly isActionMode: boolean; -} - -interface DragIndicatorInfo { - left?: number; - top?: number; - isVisible?: boolean; -} diff --git a/src/app/pages/editor/components/layertimeline/layertimelinegrid.directive.ts b/src/app/pages/editor/components/layertimeline/layertimelinegrid.directive.ts deleted file mode 100644 index c9296af2..00000000 --- a/src/app/pages/editor/components/layertimeline/layertimelinegrid.directive.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { - Directive, - ElementRef, - EventEmitter, - HostListener, - Input, - OnInit, - Output, -} from '@angular/core'; -import { Animation } from 'app/pages/editor/model/timeline'; -import { Dragger } from 'app/pages/editor/scripts/dragger'; -import { DestroyableMixin } from 'app/pages/editor/scripts/mixins'; -import { ShortcutService, ThemeService } from 'app/pages/editor/services'; -import * as $ from 'jquery'; -import * as _ from 'lodash'; -import { filter } from 'rxjs/operators'; - -import { TIMELINE_ANIMATION_PADDING } from './constants'; - -const HEADER_HEIGHT = 40; -const GRID_INTERVALS_MS = [10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000]; - -@Directive({ selector: '[appLayerTimelineGrid]' }) -export class LayerTimelineGridDirective extends DestroyableMixin() implements OnInit { - @Input() - isHeader: boolean; - @Output() - scrub = new EventEmitter(); - - private readonly canvas: HTMLCanvasElement; - private readonly $canvas: JQuery; - private animation_: Animation; - private currentTime_: number; - private horizZoom_: number; - - constructor(elementRef: ElementRef, private readonly themeService: ThemeService) { - super(); - this.canvas = elementRef.nativeElement; - this.$canvas = $(this.canvas); - } - - ngOnInit() { - this.registerSubscription( - this.themeService - .asObservable() - .pipe(filter(t => !t.isInitialPageLoad)) - .subscribe(t => this.redraw()), - ); - } - - get horizZoom() { - return this.horizZoom_; - } - - @Input() - set horizZoom(horizZoom: number) { - if (this.horizZoom_ !== horizZoom) { - this.horizZoom_ = horizZoom; - this.redraw(); - } - } - - get currentTime() { - return this.currentTime_; - } - - set currentTime(currentTime: number) { - if (this.currentTime_ !== currentTime) { - this.currentTime_ = currentTime; - this.redraw(); - } - } - - get animation() { - return this.animation_; - } - - @Input() - set animation(animation: Animation) { - this.animation_ = animation; - this.redraw(); - } - - @HostListener('mousedown', ['$event']) - onMouseDown(event: MouseEvent) { - this.handleScrubEvent(event.clientX, ShortcutService.isOsDependentModifierKey(event)); - // tslint:disable-next-line: no-unused-expression - new Dragger({ - direction: 'horizontal', - downX: event.clientX, - downY: event.clientY, - shouldSkipSlopCheck: true, - onDragFn: e => this.handleScrubEvent(e.clientX, ShortcutService.isOsDependentModifierKey(e)), - }); - event.preventDefault(); - return false; - } - - private handleScrubEvent(clientX: number, disableSnap: boolean) { - const x = clientX - this.$canvas.offset().left; - let time = - ((x - TIMELINE_ANIMATION_PADDING) / (this.$canvas.width() - TIMELINE_ANIMATION_PADDING * 2)) * - this.animation.duration; - time = _.clamp(time, 0, this.animation.duration); - this.scrub.emit({ time, disableSnap }); - } - - redraw() { - if (!this.$canvas.is(':visible')) { - return; - } - - const width = this.$canvas.width(); - const height = this.$canvas.height(); - this.$canvas.attr('width', width * window.devicePixelRatio); - this.$canvas.attr('height', height * window.devicePixelRatio); - - const ctx = this.canvas.getContext('2d'); - ctx.scale(window.devicePixelRatio, window.devicePixelRatio); - ctx.translate(TIMELINE_ANIMATION_PADDING, 0); - - // Compute grid spacing (40 = minimum grid spacing in pixels). - let interval = 0; - let spacingMs = GRID_INTERVALS_MS[interval]; - while (spacingMs * this.horizZoom < 40 || interval >= GRID_INTERVALS_MS.length) { - interval++; - spacingMs = GRID_INTERVALS_MS[interval]; - } - - const spacingPx = spacingMs * this.horizZoom; - - if (this.isHeader) { - // Text labels. - ctx.fillStyle = this.themeService.getSecondaryTextColor(); - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.font = '10px Roboto'; - for (let x = 0, t = 0; round(x) <= round(width); x += spacingPx, t += spacingMs) { - ctx.fillText(`${t / 1000}s`, x, height / 2); - } - ctx.fillStyle = 'rgba(244, 67, 54, .7)'; - ctx.beginPath(); - ctx.arc(this.currentTime * this.horizZoom, height / 2, 4, 0, 2 * Math.PI, false); - ctx.fill(); - ctx.closePath(); - ctx.fillRect(this.currentTime * this.horizZoom - 1, height / 2 + 4, 2, height); - } else { - // Grid lines. - ctx.fillStyle = this.themeService.getDividerTextColor(); - for ( - let x = spacingPx; - round(x) < round(width - TIMELINE_ANIMATION_PADDING * 2); - x += spacingPx - ) { - ctx.fillRect(x - 0.5, HEADER_HEIGHT, 1, height - HEADER_HEIGHT); - } - ctx.fillStyle = 'rgba(244, 67, 54, .7)'; - ctx.fillRect(this.currentTime * this.horizZoom - 1, HEADER_HEIGHT, 2, height - HEADER_HEIGHT); - } - } - - @HostListener('click', ['$event']) - onClick(event: MouseEvent) { - // This ensures that click events originating on top of the - // host element aren't triggered in the component. - event.stopPropagation(); - } -} - -function round(n: number) { - return _.round(n, 8); -} - -export interface ScrubEvent { - time: number; - disableSnap: boolean; -} diff --git a/src/app/pages/editor/components/layertimeline/timelineanimationrow.component.html b/src/app/pages/editor/components/layertimeline/timelineanimationrow.component.html deleted file mode 100644 index 46863d22..00000000 --- a/src/app/pages/editor/components/layertimeline/timelineanimationrow.component.html +++ /dev/null @@ -1,41 +0,0 @@ -
- - -
-
-
-
-
-
-
-
- -
    -
  • - - -
  • -
-
diff --git a/src/app/pages/editor/components/layertimeline/timelineanimationrow.component.scss b/src/app/pages/editor/components/layertimeline/timelineanimationrow.component.scss deleted file mode 100644 index 70d44757..00000000 --- a/src/app/pages/editor/components/layertimeline/timelineanimationrow.component.scss +++ /dev/null @@ -1,59 +0,0 @@ -$timelineAnimationPadding: 20px; -ul { - margin: 0; - padding: 0; -} - -li { - list-style: none; -} - -.slt-layer-row { - box-sizing: border-box; - line-height: 20px; - height: 20px; -} - -.slt-children-row { - padding: 0; - margin: 0 0 0 0px; -} - -.slt-properties { - margin: 4px 0; - border-radius: 2px; - .slt-property { - position: relative; - line-height: 24px; - height: 24px; - .slt-timeline-block { - position: absolute; - height: 12px; - border-radius: 6px; - top: 50%; - transform: translate(0, -50%); - outline: 0; - cursor: pointer; - .slt-timeline-block-edge { - position: absolute; - top: 0; - bottom: 0; - width: 6px; - cursor: ew-resize; - } - .slt-timeline-block-edge-start { - left: 0; - } - .slt-timeline-block-edge-end { - right: 0; - } - &.is-disabled { - cursor: default; - } - } - } -} - -.slt-properties-empty { - display: none; -} diff --git a/src/app/pages/editor/components/layertimeline/timelineanimationrow.component.ts b/src/app/pages/editor/components/layertimeline/timelineanimationrow.component.ts deleted file mode 100644 index d899edaa..00000000 --- a/src/app/pages/editor/components/layertimeline/timelineanimationrow.component.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - EventEmitter, - Input, - OnInit, - Output, -} from '@angular/core'; -import { Layer } from 'app/pages/editor/model/layers'; -import { Animation, AnimationBlock } from 'app/pages/editor/model/timeline'; -import { ModelUtil } from 'app/pages/editor/scripts/common'; -import { ActionModeService } from 'app/pages/editor/services'; -import { State, Store } from 'app/pages/editor/store'; -import { getTimelineAnimationRowState } from 'app/pages/editor/store/common/selectors'; -import * as _ from 'lodash'; -import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; - -@Component({ - selector: 'app-timelineanimationrow', - templateUrl: './timelineanimationrow.component.html', - styleUrls: ['./timelineanimationrow.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class TimelineAnimationRowComponent implements OnInit, Callbacks { - animationRowModel$: Observable; - - @Input() layer: Layer; - - // MouseEvents from this layer (or children layers further down the tree) - // are recursively handled by parent components until they reach - // the LayerTimelineComponent. - @Output() timelineBlockClick = new EventEmitter(); - @Output() timelineBlockMouseDown = new EventEmitter(); - @Output() timelineBlockDoubleClick = new EventEmitter(); - - constructor( - private readonly store: Store, - private readonly actionModeService: ActionModeService, - ) {} - - ngOnInit() { - this.animationRowModel$ = this.store.select(getTimelineAnimationRowState).pipe( - map(({ animation, collapsedLayerIds, selectedBlockIds, isActionMode }) => { - // Returns a list of animation block lists. Each animation block list corresponds to - // a property name displayed in the layer list tree. - const blocksByPropertyNameValues = _.values( - ModelUtil.getOrderedBlocksByPropertyByLayer(animation)[this.layer.id], - ); - return { - animation, - blocksByPropertyNameValues, - isExpanded: !collapsedLayerIds.has(this.layer.id), - selectedBlockIds, - isActionMode, - }; - }), - ); - } - - // @Override Callbacks - onTimelineBlockClick(event: MouseEvent, block: AnimationBlock) { - event.stopPropagation(); - if (!this.actionModeService.isActionMode()) { - this.timelineBlockClick.emit({ event, block }); - } - } - - // @Override Callbacks - onTimelineBlockDoubleClick(event: MouseEvent, block: AnimationBlock) { - event.stopPropagation(); - if (!this.actionModeService.isActionMode()) { - this.timelineBlockDoubleClick.emit({ event, block }); - } - } - - // @Override Callbacks - onTimelineBlockMouseDown(event: MouseEvent, block: AnimationBlock) { - if (!this.actionModeService.isActionMode()) { - this.timelineBlockMouseDown.emit({ event, block }); - } - } - - // Used by *ngFor loop. - trackLayerFn(index: number, layer: Layer) { - return layer.id; - } -} - -export interface Callbacks { - onTimelineBlockMouseDown(event: MouseEvent, block: AnimationBlock): void; - onTimelineBlockClick(event: MouseEvent, block: AnimationBlock): void; - onTimelineBlockDoubleClick(event: MouseEvent, block: AnimationBlock): void; -} - -interface AnimationRowEvent { - readonly event: MouseEvent; - readonly block: AnimationBlock; -} - -interface AnimationRowModel { - readonly animation: Animation; - readonly blocksByPropertyNameValues: ReadonlyTable; - readonly isExpanded: boolean; - readonly selectedBlockIds: ReadonlySet; - readonly isActionMode: boolean; -} diff --git a/src/app/pages/editor/components/playback/_playback-theme.scss b/src/app/pages/editor/components/playback/_playback-theme.scss deleted file mode 100644 index 4cb979dc..00000000 --- a/src/app/pages/editor/components/playback/_playback-theme.scss +++ /dev/null @@ -1,24 +0,0 @@ -@mixin ss-playback-theme($theme) { - $foreground: map-get($theme, ss-foreground); - $background: map-get($theme, ss-background); - .playback { - button { - margin: 0px 3px; - } - button { - &[mat-icon-button] { - mat-icon { - color: mat-color($foreground, secondary-text); - } - mat-icon.activated { - color: mat-color(mat-palette($mat-orange, A700)); - } - } - &[maticon-button][disabled] { - mat-icon { - color: mat-color($foreground, disabled-text); - } - } - } - } -} diff --git a/src/app/pages/editor/components/playback/index.ts b/src/app/pages/editor/components/playback/index.ts deleted file mode 100644 index 4ff9a38c..00000000 --- a/src/app/pages/editor/components/playback/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PlaybackComponent } from './playback.component'; diff --git a/src/app/pages/editor/components/playback/playback.component.html b/src/app/pages/editor/components/playback/playback.component.html deleted file mode 100644 index ebd01982..00000000 --- a/src/app/pages/editor/components/playback/playback.component.html +++ /dev/null @@ -1,50 +0,0 @@ -
- - - - - - - -
diff --git a/src/app/pages/editor/components/playback/playback.component.scss b/src/app/pages/editor/components/playback/playback.component.scss deleted file mode 100644 index fa8a241f..00000000 --- a/src/app/pages/editor/components/playback/playback.component.scss +++ /dev/null @@ -1,54 +0,0 @@ -.playback { - button { - margin: 0px 3px; - } - .play-pause-icon { - $numAnimationFrames: 21; - position: absolute; - overflow: hidden; - left: 50%; - top: 50%; - width: 24px; - height: 24px; - transform: translate(-50%, -50%); - &::after { - content: ''; - display: block; - pointer-events: none; - position: absolute; - left: 0; - top: 0; - width: ($numAnimationFrames * 24px); - height: 24px; - background-size: ($numAnimationFrames * 24px) 24px; - background-position: 0% 0%; - background-image: url(/assets/icons/pauseplay-white.png); - transform: translateX(($numAnimationFrames - 1) * -24px); - animation-duration: (1s * $numAnimationFrames / 60); - animation-timing-function: steps($numAnimationFrames - 1); - } - &.can-animate::after { - animation-name: pauseplay; - } - &.can-animate.is-playing::after { - background-image: url(/assets/icons/playpause-white.png); - animation-name: playpause; - } - @keyframes playpause { - from { - transform: translateX(0); - } - to { - transform: translateX(($numAnimationFrames - 1) * -24px); - } - } - @keyframes pauseplay { - from { - transform: translateX(0); - } - to { - transform: translateX(($numAnimationFrames - 1) * -24px); - } - } - } -} diff --git a/src/app/pages/editor/components/playback/playback.component.ts b/src/app/pages/editor/components/playback/playback.component.ts deleted file mode 100644 index af7f818b..00000000 --- a/src/app/pages/editor/components/playback/playback.component.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { PlaybackService } from 'app/pages/editor/services'; -import { State, Store } from 'app/pages/editor/store'; -import { getPlaybackState } from 'app/pages/editor/store/playback/selectors'; -import { Observable } from 'rxjs'; - -@Component({ - selector: 'app-playback', - templateUrl: './playback.component.html', - styleUrls: ['./playback.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class PlaybackComponent implements OnInit { - playbackModel$: Observable; - - constructor( - private readonly store: Store, - private readonly playbackService: PlaybackService, - ) {} - - ngOnInit() { - this.playbackModel$ = this.store.select(getPlaybackState); - } - - isSlowMotionClick(event: MouseEvent) { - event.stopPropagation(); - this.playbackService.toggleIsSlowMotion(); - } - - rewindClick(event: MouseEvent) { - event.stopPropagation(); - this.playbackService.rewind(); - } - - playPauseButtonClick(event: MouseEvent) { - event.stopPropagation(); - this.playbackService.toggleIsPlaying(); - } - - fastForwardClick(event: MouseEvent) { - event.stopPropagation(); - this.playbackService.fastForward(); - } - - isRepeatingClick(event: MouseEvent) { - event.stopPropagation(); - this.playbackService.toggleIsRepeating(); - } -} - -interface PlaybackModel { - readonly isSlowMotion: boolean; - readonly isPlaying: boolean; - readonly isRepeating: boolean; -} diff --git a/src/app/pages/editor/components/project/index.ts b/src/app/pages/editor/components/project/index.ts deleted file mode 100644 index 1ff693b9..00000000 --- a/src/app/pages/editor/components/project/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ProjectService } from './project.service'; diff --git a/src/app/pages/editor/components/project/project.service.ts b/src/app/pages/editor/components/project/project.service.ts deleted file mode 100644 index 439d870d..00000000 --- a/src/app/pages/editor/components/project/project.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; -import { VectorLayer } from 'app/pages/editor/model/layers'; -import { Animation } from 'app/pages/editor/model/timeline'; -import { ModelUtil } from 'app/pages/editor/scripts/common'; -import { FileExportService } from 'app/pages/editor/services/fileexport.service'; - -// TODO: store hidden layer IDs and vector layer inside the animations? -interface Project { - readonly vectorLayer: VectorLayer; - readonly animation: Animation; - readonly hiddenLayerIds: ReadonlySet; -} - -@Injectable({ providedIn: 'root' }) -export class ProjectService { - constructor(private readonly http: HttpClient) {} - - /** - * Fetches a shape shifter project via HTTP. - * @param url the URL of the shape shifter project - */ - getProject(url: string): Promise { - return this.http - .get(url) - .toPromise() - .then(response => { - const jsonObj = response; - const { vectorLayer, animation, hiddenLayerIds } = FileExportService.fromJSON(jsonObj); - return ModelUtil.regenerateModelIds(vectorLayer, animation, hiddenLayerIds) as Project; - }); - } -} diff --git a/src/app/pages/editor/components/propertyinput/InspectedProperty.ts b/src/app/pages/editor/components/propertyinput/InspectedProperty.ts deleted file mode 100644 index 6bbe6dc1..00000000 --- a/src/app/pages/editor/components/propertyinput/InspectedProperty.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Property } from 'app/pages/editor/model/properties'; - -/** - * Stores information about an inspected property. - * V is the property value type (number, string, or path). - */ -export class InspectedProperty { - readonly typeName: string; - - constructor( - // The model object being inspected (a layer, animation, or animation block). - model: any, - // The model object's inspected property. - readonly property: Property, - // The model object's inspected property name. - readonly propertyName: string, - // The in-memory entered value map. - private readonly enteredValueMap: Map, - // Stores the model's entered value for the given property name in the application store. - private readonly setValueFn: (value: V) => void, - // Returns the value associated with this model's property name. - private readonly getValueFn = () => model[propertyName], - // Provides an opportunity to edit the value before it is set. - private readonly transformEditedValueFn = (enteredValue: V) => enteredValue, - // Returns whether or not this property name is editable. - readonly isEditable = () => true, - ) { - this.typeName = this.property.getTypeName(); - } - - get value() { - return this.getValueFn(); - } - - set value(value: V) { - this.setValueFn(value); - } - - getDisplayValue() { - return this.property.displayValueForValue(this.value); - } - - get editableValue() { - const enteredValue = this.getEnteredValue(); - return enteredValue === undefined - ? this.property.getEditableValue(this, 'value') - : enteredValue; - } - - set editableValue(enteredValue: V) { - this.setEnteredValue(enteredValue); - enteredValue = this.transformEditedValueFn(enteredValue); - this.property.setEditableValue(this, 'value', enteredValue); - } - - resolveEnteredValue() { - this.setEnteredValue(undefined); - } - - private getEnteredValue() { - if (this.enteredValueMap.has(this.propertyName)) { - return this.enteredValueMap.get(this.propertyName); - } - return undefined; - } - - private setEnteredValue(value: any) { - if (value === undefined) { - this.enteredValueMap.delete(this.propertyName); - } else { - this.enteredValueMap.set(this.propertyName, value); - } - } -} diff --git a/src/app/pages/editor/components/propertyinput/_propertyinput-theme.scss b/src/app/pages/editor/components/propertyinput/_propertyinput-theme.scss deleted file mode 100644 index 624db8d1..00000000 --- a/src/app/pages/editor/components/propertyinput/_propertyinput-theme.scss +++ /dev/null @@ -1,66 +0,0 @@ -@mixin ss-propertyinput-theme($theme) { - $foreground: map-get($theme, ss-foreground); - $background: map-get($theme, ss-background); - $accent: map-get($theme, accent); - $is-dark: map-get($theme, is-dark); - .property-input { - background-color: mat-color($background, base200); - } - .spi-header { - background-color: mat-color($background, base100); - .spi-selection-icon { - color: mat-color($foreground, secondary-text); - } - button.spi-secondary-icon { - color: mat-color($foreground, secondary-text); - &[mat-icon-button][disabled] { - mat-icon { - color: mat-color($foreground, disabled-text); - } - } - } - .spi-selection-description { - color: mat-color($foreground, primary-text); - } - .spi-selection-sub-description { - color: mat-color($foreground, secondary-text); - } - } - .spi-body { - .spi-property-name { - color: mat-color($foreground, secondary-text); - } - .spi-property-value-static, - .spi-property-value-editor input, - .spi-property-value-menu-target { - box-shadow: 0 0 0 1px mat-color($foreground, divider) inset; - &.has-input-error { - border-left: 4px solid mat-color(mat-palette($mat-red, A700)); - } - } - .spi-property-value-editor input, - .spi-property-value-menu-target { - background-color: mat-color($background, base); - color: mat-color($foreground, primary-text); - &:focus { - box-shadow: 0 0 0 1px mat-color($accent) inset; - } - } - .spi-property-value-menu-target { - &::after { - color: mat-color($foreground, disabled-text); - } - } - .spi-property-color-preview { - box-shadow: 0 0 0 1px mat-color($foreground, divider) inset; - } - } - .spi-empty { - color: mat-color($foreground, disabled-text); - } - .alert-danger { - color: mat-color(mat-palette($mat-red, A700)); - background-color: mat-color(mat-palette($mat-red, 50)); - border-color: mat-color(mat-palette($mat-red, 100)); - } -} diff --git a/src/app/pages/editor/components/propertyinput/index.ts b/src/app/pages/editor/components/propertyinput/index.ts deleted file mode 100644 index 03ffc58f..00000000 --- a/src/app/pages/editor/components/propertyinput/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { PropertyInputComponent } from './propertyinput.component'; diff --git a/src/app/pages/editor/components/propertyinput/propertyinput.component.html b/src/app/pages/editor/components/propertyinput/propertyinput.component.html deleted file mode 100644 index 45d4e2fb..00000000 --- a/src/app/pages/editor/components/propertyinput/propertyinput.component.html +++ /dev/null @@ -1,146 +0,0 @@ -
- - - - -
- Select something to edit its properties -
- -
-
- - -
- {{ model.description }} - {{ model.subDescription }} -
- - - - - -
-
-
- No shared properties to view or edit -
-
-
{{ ip.propertyName }}
-
-
-
- - - {{ ip.getDisplayValue() }} - -
- - - - - - - -
-
-
-
- - - -
-
-
-
-
diff --git a/src/app/pages/editor/components/propertyinput/propertyinput.component.scss b/src/app/pages/editor/components/propertyinput/propertyinput.component.scss deleted file mode 100644 index 31c065eb..00000000 --- a/src/app/pages/editor/components/propertyinput/propertyinput.component.scss +++ /dev/null @@ -1,144 +0,0 @@ -@import '../../styles/material-icons'; - -.property-input { - width: 320px; - user-select: none; - cursor: default; - position: relative; -} - -.spi-header { - z-index: 1; - display: flex; - flex-direction: row; - padding: 16px; - .spi-selection-icon { - margin-right: 12px; - width: 32px; - height: 32px; - } - .spi-selection-description-container { - padding: (32px - 24px) / 2 0; - } - .spi-selection-description { - font-weight: 500; - font-size: 20px; - line-height: 24px; - } - .spi-selection-sub-description { - font-size: 14px; - line-height: 20px; - &:empty { - display: none; - } - } -} - -.spi-body { - overflow-y: auto; - padding: 8px 0; - .spi-property { - display: flex; - flex-direction: row; - padding: 4px 16px; - min-height: 24px; - } - .spi-property-name { - font-size: 14px; - line-height: 24px; - flex: 1 1 0; - } - .spi-property-value { - flex: 2 1 0; - min-width: 0; - font-size: 14px; - line-height: 24px; - } - .spi-property-value-static, - .spi-property-value-editor input, - .spi-property-value-menu-target { - border: 0; - border-radius: 2px; - font-size: 14px; - line-height: 20px; - height: 24px; - box-sizing: border-box; - padding: 2px 6px; - outline: 0; - } - .spi-property-value-static { - overflow-x: scroll; - white-space: nowrap; - overflow-y: hidden; - -ms-overflow-style: none; - &::-webkit-scrollbar { - display: none; - } - user-select: all; - cursor: text; - } - .spi-property-value-menu-target { - border: 0; - text-align: left; - position: relative; - padding-right: 24px; - &::after { - @include material-icons; - content: 'arrow_drop_down'; - position: absolute; - font-size: 24px; - right: 0; - top: 50%; - transform: translate(0, -50%); - } - } - .spi-property-value-menu-current-value { - display: block; - width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .spi-property-color-preview { - width: 16px; - height: 16px; - min-width: 16px; - min-height: 16px; - margin: 4px 8px 4px 4px; - border-radius: 50%; - } -} - -.spi-empty { - padding: 32px; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - font-size: 14px; - line-height: 20px; -} - -.launch-shape-shifter-button { - min-height: 72px; - text-transform: uppercase; -} - -.alert { - padding: 16px; - margin-top: 6px; - margin-left: 16px; - margin-right: 16px; - margin-bottom: 16px; - border: 1px solid transparent; - border-radius: 4px; -} - -.auto-fix-button { - margin-left: 8px; -} - -.paths-incompatible-text { - font-size: 14px; - line-height: 20px; -} diff --git a/src/app/pages/editor/components/propertyinput/propertyinput.component.ts b/src/app/pages/editor/components/propertyinput/propertyinput.component.ts deleted file mode 100644 index 111551f7..00000000 --- a/src/app/pages/editor/components/propertyinput/propertyinput.component.ts +++ /dev/null @@ -1,371 +0,0 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { ActionMode } from 'app/pages/editor/model/actionmode'; -import { - ClipPathLayer, - GroupLayer, - Layer, - LayerUtil, - PathLayer, - VectorLayer, -} from 'app/pages/editor/model/layers'; -import { FractionProperty, NameProperty, Option } from 'app/pages/editor/model/properties'; -import { Animation, PathAnimationBlock } from 'app/pages/editor/model/timeline'; -import { ColorUtil, ModelUtil } from 'app/pages/editor/scripts/common'; -import { - ActionModeService, - LayerTimelineService, - PlaybackService, - ShortcutService, - ThemeService, -} from 'app/pages/editor/services'; -import { State, Store } from 'app/pages/editor/store'; -import { getPropertyInputState } from 'app/pages/editor/store/common/selectors'; -import { ThemeType } from 'app/pages/editor/store/theme/reducer'; -import { SetAnimation } from 'app/pages/editor/store/timeline/actions'; -import * as $ from 'jquery'; -import * as _ from 'lodash'; -import { Observable , combineLatest } from 'rxjs'; -import { map } from 'rxjs/operators'; - -import { InspectedProperty } from './InspectedProperty'; - -declare const ga: Function; - -// TODO: when you enter a 'start time' larger than 'end time', transform 'end time' correctly -@Component({ - selector: 'app-propertyinput', - templateUrl: './propertyinput.component.html', - styleUrls: ['./propertyinput.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class PropertyInputComponent implements OnInit { - propertyInputModel$: Observable; - - // Map used to track user state that has been entered into textfields - // but may not have been saved in the store. - private readonly enteredValueMap = new Map(); - - themeState$: Observable<{ prevThemeType: ThemeType; currThemeType: ThemeType }>; - - constructor( - private readonly store: Store, - private readonly actionModeService: ActionModeService, - private readonly playbackService: PlaybackService, - private readonly layerTimelineService: LayerTimelineService, - readonly themeService: ThemeService, - ) {} - - ngOnInit() { - let prevThemeType: ThemeType; - let currThemeType = this.themeService.getThemeType().themeType; - this.propertyInputModel$ = this.store.select(getPropertyInputState).pipe( - map(({ animation, isAnimationSelected, selectedBlockIds, vectorLayer, selectedLayerIds }) => { - prevThemeType = currThemeType = this.themeService.getThemeType().themeType; - if (selectedLayerIds.size) { - return this.buildInspectedLayerProperties(vectorLayer, selectedLayerIds, animation); - } else if (selectedBlockIds.size) { - return this.buildInspectedBlockProperties(vectorLayer, animation, selectedBlockIds); - } else if (isAnimationSelected) { - return this.buildInspectedAnimationProperties(animation); - } else { - return { - numSelections: 0, - inspectedProperties: [], - availablePropertyNames: [], - } as PropertyInputModel; - } - }), - ); - this.themeState$ = combineLatest( - this.propertyInputModel$, - this.themeService.asObservable(), - ).pipe( - map(([unused, { themeType }]) => { - prevThemeType = currThemeType; - currThemeType = this.themeService.getThemeType().themeType; - return { prevThemeType, currThemeType }; - }), - ); - } - - shouldShowStartActionModeButton(pim: PropertyInputModel) { - return pim.numSelections === 1 && pim.model instanceof PathAnimationBlock; - } - - shouldDisableStartActionModeButton(pim: PropertyInputModel) { - if (!this.shouldShowStartActionModeButton(pim)) { - return false; - } - const { fromValue, toValue } = pim.model as PathAnimationBlock; - return !fromValue || !fromValue.getPathString() || !toValue || !toValue.getPathString(); - } - - onAutoFixPathsClick(pim: PropertyInputModel) { - this.actionModeService.autoFix(); - } - - onStartActionModeClick() { - ga('send', 'event', 'Action mode', 'Started'); - this.actionModeService.setActionMode(ActionMode.Selection); - } - - shouldShowAnimateLayerButton(pim: PropertyInputModel) { - return ( - pim.availablePropertyNames.length > 0 && - pim.numSelections === 1 && - (pim.model instanceof VectorLayer || - pim.model instanceof GroupLayer || - pim.model instanceof ClipPathLayer || - pim.model instanceof PathLayer) - ); - } - - onAnimateLayerClick(layer: Layer, propertyName: string) { - const clonedValue = layer.inspectableProperties - .get(propertyName) - .cloneValue((layer as any)[propertyName]); - const currentTime = this.playbackService.getCurrentTime(); - this.layerTimelineService.addBlocks([ - { - layerId: layer.id, - propertyName, - fromValue: clonedValue, - toValue: clonedValue, - currentTime, - }, - ]); - } - - shouldShowInvalidPathAnimationBlockMsg(pim: PropertyInputModel) { - return ( - pim.numSelections === 1 && - pim.model instanceof PathAnimationBlock && - !pim.model.isAnimatable() - ); - } - - isPathBlockFromValueEmpty(block: PathAnimationBlock) { - return !block.fromValue || !block.fromValue.getPathString(); - } - - isPathBlockToValueEmpty(block: PathAnimationBlock) { - return !block.toValue || !block.toValue.getPathString(); - } - - onValueEditorKeyDown(event: KeyboardEvent, ip: InspectedProperty) { - switch (event.keyCode) { - // Up/down arrow buttons. - case 38: - case 40: - ip.resolveEnteredValue(); - const $target = $(event.target) as JQuery; - const numberValue = Number($target.val()); - if (isNaN(numberValue)) { - break; - } - let delta = event.keyCode === 38 ? 1 : -1; - - if (ip.property instanceof FractionProperty) { - delta *= 0.1; - } - - if (event.shiftKey) { - // TODO: make this more obvious somehow - delta *= 10; - } else if (ShortcutService.isOsDependentModifierKey(event)) { - // TODO: make this more obvious somehow - delta /= 10; - } - - ip.property.setEditableValue(ip, 'value', Number((numberValue + delta).toFixed(6))); - setTimeout(() => $target.get(0).select(), 0); - return false; - } - return undefined; - } - - private buildInspectedLayerProperties( - vl: VectorLayer, - selectedLayerIds: ReadonlySet, - animation: Animation, - ) { - const numSelections = selectedLayerIds.size; - const selectedLayers = Array.from(selectedLayerIds).map(id => vl.findLayerById(id)); - if (numSelections > 1) { - return { - numSelections, - icon: 'collection', - description: `${numSelections} layers`, - // TODO: implement batch editting - inspectedProperties: [], - availablePropertyNames: [], - } as PropertyInputModel; - } - // Edit a single layer. - const enteredValueMap = this.enteredValueMap; - const layer = selectedLayers[0]; - const icon = layer.type; - const description = layer.name; - const inspectedProperties: InspectedProperty[] = []; - layer.inspectableProperties.forEach((property, propertyName) => { - inspectedProperties.push( - new InspectedProperty( - layer, - property, - propertyName, - enteredValueMap, - value => { - // TODO: avoid dispatching the action if the properties are equal - const clonedLayer: any = layer.clone(); - clonedLayer[propertyName] = value; - this.layerTimelineService.updateLayer(clonedLayer); - }, - // TODO: return the 'rendered' value if an animation is ongoing? (see AIA) - undefined, - enteredValue => { - if (property instanceof NameProperty) { - return LayerUtil.getUniqueLayerName([vl], NameProperty.sanitize(enteredValue)); - } - return enteredValue; - }, - // TODO: copy AIA conditions to determine whether this should be editable - undefined, - ), - ); - }); - const availablePropertyNames = Array.from( - ModelUtil.getAvailablePropertyNamesForLayer(layer, animation), - ); - return { - model: layer, - numSelections, - inspectedProperties, - icon, - description, - availablePropertyNames, - } as PropertyInputModel; - } - - private buildInspectedBlockProperties( - vl: VectorLayer, - animation: Animation, - selectedBlockIds: ReadonlySet, - ) { - const numSelections = selectedBlockIds.size; - const selectedBlocks = Array.from(selectedBlockIds).map(id => { - return _.find(animation.blocks, b => b.id === id); - }); - if (numSelections > 1) { - return { - numSelections, - icon: 'collection', - // TODO: implement batch editting - description: `${numSelections} property animations`, - inspectedProperties: [], - availablePropertyNames: [], - } as PropertyInputModel; - } - const enteredValueMap = this.enteredValueMap; - const block = selectedBlocks[0]; - const icon = 'animationblock'; - const description = block.propertyName; - const blockLayer = vl.findLayerById(block.layerId); - const subDescription = `for '${blockLayer.name}'`; - const inspectedProperties: InspectedProperty[] = []; - block.inspectableProperties.forEach((property, propertyName) => { - inspectedProperties.push( - new InspectedProperty(block, property, propertyName, enteredValueMap, value => { - // TODO: avoid dispatching the action if the properties are equal - const clonedBlock: any = block.clone(); - clonedBlock[propertyName] = value; - this.layerTimelineService.updateBlocks([clonedBlock]); - }), - ); - }); - return { - model: block, - numSelections, - inspectedProperties, - icon, - description, - subDescription, - availablePropertyNames: [], - } as PropertyInputModel; - } - - private buildInspectedAnimationProperties(animation: Animation) { - const store = this.store; - const enteredValueMap = this.enteredValueMap; - const icon = 'animation'; - const description = animation.name; - const inspectedProperties: InspectedProperty[] = []; - animation.inspectableProperties.forEach((property, propertyName) => { - inspectedProperties.push( - new InspectedProperty( - animation, - property, - propertyName, - enteredValueMap, - value => { - // TODO: avoid dispatching the action if the properties are equal - const clonedAnimation: any = animation.clone(); - clonedAnimation[propertyName] = value; - store.dispatch(new SetAnimation(clonedAnimation)); - }, - undefined, - undefined, - undefined, - ), - ); - }); - return { - model: animation, - numSelections: 1, - inspectedProperties, - icon, - description, - availablePropertyNames: [], - } as PropertyInputModel; - } - - // Called from the HTML template. - androidToCssColor(color: string) { - return ColorUtil.androidToCssHexColor(color); - } - - trackInspectedPropertyFn(index: number, ip: InspectedProperty) { - return ip.propertyName; - } - - trackEnumOptionFn(index: number, option: Option) { - return option.value; - } -} - -// TODO: use this for batch editing -// function getSharedPropertyNames(items: ReadonlyArray) { -// if (!items || !items.length) { -// return []; -// } -// let shared: ReadonlyArray; -// items.forEach(item => { -// const names = Array.from(item.inspectableProperties.keys()); -// if (!shared) { -// shared = names; -// } else { -// shared = shared.filter(n => names.includes(n)); -// } -// }); -// return shared; -// } - -interface PropertyInputModel { - readonly model?: any; - readonly numSelections: number; - readonly inspectedProperties: ReadonlyArray>; - // TODO: use a union type here for better type safety? - readonly icon?: string; - readonly description?: string; - readonly subDescription?: string; - readonly availablePropertyNames: ReadonlyArray; -} diff --git a/src/app/pages/editor/components/root/_root-theme.scss b/src/app/pages/editor/components/root/_root-theme.scss deleted file mode 100644 index 3393c6bb..00000000 --- a/src/app/pages/editor/components/root/_root-theme.scss +++ /dev/null @@ -1,13 +0,0 @@ -@mixin ss-root-theme($theme) { - $background: map-get($theme, ss-background); - $accent: map-get($theme, accent); - div.display-container { - background-color: mat-color($background, base300); - } - .file-drop-target { - &.is-dragging-over::after { - background-color: mat-color($accent, darker, 0.5); - box-shadow: 0 0 0 8px mat-color($accent, darker) inset; - } - } -} diff --git a/src/app/pages/editor/components/root/droptarget.directive.ts b/src/app/pages/editor/components/root/droptarget.directive.ts deleted file mode 100644 index 6e212c42..00000000 --- a/src/app/pages/editor/components/root/droptarget.directive.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Directive, ElementRef, EventEmitter, HostListener, OnInit, Output } from '@angular/core'; -import * as $ from 'jquery'; - -enum DragState { - None = 0, - Dragging, - Loading, -} - -@Directive({ - selector: '[appDropTarget]', -}) -export class DropTargetDirective implements OnInit { - @Output() - onDropFiles = new EventEmitter(); - - private element: JQuery; - private dragState = DragState.None; - private notDraggingTimeoutId: number; - - constructor(private readonly elementRef: ElementRef) {} - - ngOnInit() { - this.element = $(this.elementRef.nativeElement); - this.element.addClass('file-drop-target'); - } - - @HostListener('dragenter', ['$event']) - onDragEnter(event: Event) { - event.preventDefault(); - this.setDragging(true); - return false; - } - - @HostListener('dragover', ['$event']) - onDragOver(event: Event) { - event.preventDefault(); - (event as DragEvent).dataTransfer.dropEffect = 'copy'; - return false; - } - - @HostListener('dragleave', ['$event']) - onDragLeave(event: Event) { - event.preventDefault(); - this.setDragging(false); - return false; - } - - @HostListener('drop', ['$event']) - onDrop(event: Event) { - this.setDragState(DragState.None); - this.onDropFiles.emit((event as DragEvent).dataTransfer.files); - return false; - } - - private setDragState(state: DragState) { - this.dragState = state; - this.element.toggleClass('is-dragging-over', this.dragState === DragState.Dragging); - this.element.toggleClass('is-loading', this.dragState === DragState.Loading); - } - - // Set up drag event listeners, with debouncing because dragging over/out - // of each child triggers these events on the element. - private setDragging(isDragging: boolean) { - if (isDragging) { - // When moving from child to child, dragenter is sent before dragleave - // on previous child. - window.setTimeout(() => { - if (this.notDraggingTimeoutId) { - window.clearTimeout(this.notDraggingTimeoutId); - this.notDraggingTimeoutId = undefined; - } - this.setDragState(DragState.Dragging); - }, 0); - } else { - if (this.notDraggingTimeoutId) { - window.clearTimeout(this.notDraggingTimeoutId); - } - this.notDraggingTimeoutId = window.setTimeout(() => this.setDragState(DragState.None), 100); - } - } -} diff --git a/src/app/pages/editor/components/root/root.component.html b/src/app/pages/editor/components/root/root.component.html deleted file mode 100644 index d88e2246..00000000 --- a/src/app/pages/editor/components/root/root.component.html +++ /dev/null @@ -1,63 +0,0 @@ -
- - - - -
- -
- -
-
- - - -
- -
- - - - - - -
- - -
- - - -
- - -
-
\ No newline at end of file diff --git a/src/app/pages/editor/components/root/root.component.scss b/src/app/pages/editor/components/root/root.component.scss deleted file mode 100644 index 49205bc9..00000000 --- a/src/app/pages/editor/components/root/root.component.scss +++ /dev/null @@ -1,155 +0,0 @@ -:host, -.app-container { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - user-select: none; -} - -app-canvas.start { - margin-right: 48px; -} - -app-canvas.end { - margin-left: 48px; -} - -div.toolbar-container { - position: relative; - z-index: 1; -} - -.file-drop-target { - &.is-dragging-over::after { - content: ''; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - z-index: 9999; - animation: pulsate-color 0.66s ease 0s infinite; - pointer-events: none; - transform: translateZ(0); - } - @keyframes pulsate-color { - 0% { - opacity: 0.5; - } - 50% { - opacity: 1; - } - 100% { - opacity: 0.5; - } - } -} - -div.display-container { - box-sizing: border-box; - padding-bottom: 16px; - user-select: none; -} - -app-toolpanel { - // TODO: make this color theme dependent - background-color: white; -} - -.cursor-default { - cursor: default; -} - -.cursor-pointer { - cursor: pointer; -} - -.cursor-resize0 { - cursor: ns-resize; -} - -.cursor-resize45 { - cursor: nesw-resize; -} - -.cursor-resize90 { - cursor: ew-resize; -} - -.cursor-resize135 { - cursor: nwse-resize; -} - -.cursor-zoom-in { - cursor: zoom-in; -} - -.cursor-zoom-out { - cursor: zoom-out; -} - -.cursor-grab { - cursor: grab; -} - -.cursor-grabbing { - cursor: grabbing; -} - -.cursor-point-select { - cursor: url(/assets/cursor/point-select.png) 3 3, auto; -} - -.cursor-crosshair { - cursor: url(/assets/cursor/crosshair.png) 10 10, auto; -} - -.cursor-pen { - cursor: url(/assets/cursor/pen.png) 6 0, auto; -} - -.cursor-pen-add { - cursor: url(/assets/cursor/pen-add.png) 6 0, auto; -} - -.cursor-pen-close { - cursor: url(/assets/cursor/pen-close.png) 6 0, auto; -} - -.cursor-pencil { - cursor: url(/assets/cursor/pencil.png) 0 18, auto; -} - -.cursor-rotate0 { - cursor: url(/assets/cursor/rotate-top.png) 11 11, auto; -} - -.cursor-rotate45 { - cursor: url(/assets/cursor/rotate-top-right.png) 10 11, auto; -} - -.cursor-rotate90 { - cursor: url(/assets/cursor/rotate-right.png) 10 12, auto; -} - -.cursor-rotate135 { - cursor: url(/assets/cursor/rotate-bottom-right.png) 11 11, auto; -} - -.cursor-rotate180 { - cursor: url(/assets/cursor/rotate-bottom.png) 11 10, auto; -} - -.cursor-rotate225 { - cursor: url(/assets/cursor/rotate-bottom-left.png) 11 10, auto; -} - -.cursor-rotate270 { - cursor: url(/assets/cursor/rotate-left.png) 11 12, auto; -} - -.cursor-rotate315 { - cursor: url(/assets/cursor/rotate-top-left.png) 10 10, auto; -} diff --git a/src/app/pages/editor/components/root/root.component.ts b/src/app/pages/editor/components/root/root.component.ts deleted file mode 100644 index 1f0d945d..00000000 --- a/src/app/pages/editor/components/root/root.component.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { OverlayContainer } from '@angular/cdk/overlay'; -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - ElementRef, - HostBinding, - OnDestroy, - OnInit, - ViewChild, -} from '@angular/core'; -import { DialogService, DropFilesAction } from 'app/pages/editor/components/dialogs'; -import { ProjectService } from 'app/pages/editor/components/project'; -import { ActionMode, ActionSource } from 'app/pages/editor/model/actionmode'; -import { CursorType } from 'app/pages/editor/model/paper'; -import { bugsnagClient } from 'app/pages/editor/scripts/bugsnag'; -import { DestroyableMixin } from 'app/pages/editor/scripts/mixins'; -import { - ActionModeService, - ClipboardService, - FileImportService, - LayerTimelineService, - ShortcutService, - ThemeService, -} from 'app/pages/editor/services'; -import { Duration, SnackBarService } from 'app/pages/editor/services/snackbar.service'; -import { State, Store } from 'app/pages/editor/store'; -import { getActionMode, getActionModeHover } from 'app/pages/editor/store/actionmode/selectors'; -import { isWorkspaceDirty } from 'app/pages/editor/store/common/selectors'; -import { getCursorType } from 'app/pages/editor/store/paper/selectors'; -import { ResetWorkspace } from 'app/pages/editor/store/reset/actions'; -import * as erd from 'element-resize-detector'; -import { environment } from 'environments/environment'; -import * as $ from 'jquery'; -import { BehaviorSubject, Observable, combineLatest } from 'rxjs'; -import { distinctUntilChanged, first, map } from 'rxjs/operators'; - -const IS_DEV_BUILD = !environment.production; -const ELEMENT_RESIZE_DETECTOR = erd({ strategy: 'scroll' }); -const STORAGE_KEY_FIRST_TIME_USER = 'storage_key_first_time_user'; - -@Component({ - selector: 'app-root', - templateUrl: './root.component.html', - styleUrls: ['./root.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class RootComponent extends DestroyableMixin() implements OnInit, AfterViewInit, OnDestroy { - readonly ACTION_SOURCE_FROM = ActionSource.From; - readonly ACTION_SOURCE_ANIMATED = ActionSource.Animated; - readonly ACTION_SOURCE_TO = ActionSource.To; - - readonly IS_BETA = environment.beta; - - @HostBinding('class.ss-dark-theme') - isDarkThemeHostBinding: boolean; - @ViewChild('displayContainer') - displayContainerRef: ElementRef; - private $displayContainer: JQuery; - - private readonly displayBoundsSubject = new BehaviorSubject({ w: 1, h: 1 }); - canvasBounds$: Observable; - isActionMode$: Observable; - cursorClassName$: Observable; - - constructor( - private readonly snackBarService: SnackBarService, - private readonly fileImportService: FileImportService, - private readonly store: Store, - private readonly actionModeService: ActionModeService, - private readonly shortcutService: ShortcutService, - private readonly demoService: ProjectService, - private readonly dialogService: DialogService, - private readonly clipboardService: ClipboardService, - private readonly layerTimelineService: LayerTimelineService, - readonly themeService: ThemeService, - private readonly overlayContainer: OverlayContainer, - ) { - super(); - } - - ngOnInit() { - this.shortcutService.init(); - this.clipboardService.init(); - - this.registerSubscription( - this.themeService.asObservable().subscribe(t => { - const isDark = t.themeType === 'dark'; - this.isDarkThemeHostBinding = isDark; - const { classList } = this.overlayContainer.getContainerElement(); - if (isDark) { - classList.add('ss-dark-theme'); - } else { - classList.remove('ss-dark-theme'); - } - }), - ); - - $(window).on('beforeunload', event => { - let isDirty: boolean; - this.store - .select(isWorkspaceDirty) - .pipe(first()) - .subscribe(dirty => (isDirty = dirty)); - if (isDirty && !IS_DEV_BUILD) { - return `You've made changes but haven't saved. Are you sure you want to navigate away?`; - } - return undefined; - }); - - const displaySize$ = this.displayBoundsSubject.asObservable().pipe( - distinctUntilChanged((s1, s2) => { - return s1.w === s2.w && s1.h === s2.h; - }), - ); - this.isActionMode$ = this.store - .select(getActionMode) - .pipe(map(mode => mode !== ActionMode.None)); - this.canvasBounds$ = combineLatest(displaySize$, this.isActionMode$).pipe( - map(([{ w, h }, shouldShowThreeCanvases]) => { - return { w: w / (shouldShowThreeCanvases ? 3 : 1), h }; - }), - ); - - this.cursorClassName$ = combineLatest( - this.store.select(getCursorType), - this.store.select(getActionMode), - this.store.select(getActionModeHover), - ).pipe( - map(([cursorType, mode, hover]) => { - if (mode === ActionMode.SplitCommands || mode === ActionMode.SplitSubPaths) { - return CursorType.Pen; - } else if (hover) { - return CursorType.Pointer; - } - return cursorType || CursorType.Default; - }), - map(cursorType => `cursor-${cursorType}`), - ); - } - - ngAfterViewInit() { - if (!this.isMobile()) { - this.$displayContainer = $(this.displayContainerRef.nativeElement); - ELEMENT_RESIZE_DETECTOR.listenTo(this.$displayContainer.get(0), () => { - const w = this.$displayContainer.width(); - const h = this.$displayContainer.height(); - this.displayBoundsSubject.next({ w, h }); - }); - } - - if ('serviceWorker' in navigator) { - const isFirstTimeUser = window.localStorage.getItem(STORAGE_KEY_FIRST_TIME_USER); - if (!isFirstTimeUser) { - window.localStorage.setItem(STORAGE_KEY_FIRST_TIME_USER, 'true'); - setTimeout(() => { - this.snackBarService.show('Ready to work offline', 'Dismiss', Duration.Long); - }); - } - } - - const projectUrl = getUrlParameter('project'); - if (projectUrl) { - this.demoService - .getProject(projectUrl) - .then(({ vectorLayer, animation, hiddenLayerIds }) => { - this.store.dispatch(new ResetWorkspace(vectorLayer, animation, hiddenLayerIds)); - }) - .catch(e => { - this.snackBarService.show( - `There was a problem loading the Shape Shifter project`, - 'Dismiss', - Duration.Long, - ); - }); - } - } - - ngOnDestroy() { - super.ngOnDestroy(); - if (!this.isMobile()) { - ELEMENT_RESIZE_DETECTOR.removeAllListeners(this.$displayContainer.get(0)); - } - this.shortcutService.destroy(); - this.clipboardService.destroy(); - $(window).unbind('beforeunload'); - } - - // Called by the DropTargetDirective. - onDropFiles(fileList: FileList) { - if (this.actionModeService.isActionMode()) { - // TODO: make action mode automatically exit when layers/blocks are added in other parts of the app - bugsnagClient.notify('Attempt to import files while in action mode', { - severity: 'warning', - }); - return; - } - if (!fileList || !fileList.length) { - return; - } - const files: File[] = []; - // tslint:disable-next-line: prefer-for-of - for (let i = 0; i < fileList.length; i++) { - files.push(fileList[i]); - } - const type = files[0].type; - if (!files.every(file => file.type === type)) { - // TODO: handle attempts to import different types of files better - return; - } - if (type === 'application/json' || files[0].name.match(/\.shapeshifter$/)) { - // TODO: Show a dialog here as well? - this.fileImportService.import(fileList, true /* resetWorkspace */); - return; - } - - this.dialogService.dropFiles().subscribe(action => { - if (action === DropFilesAction.AddToWorkspace) { - this.fileImportService.import(fileList); - } else if (action === DropFilesAction.ResetWorkspace) { - this.fileImportService.import(fileList, true /* resetWorkspace */); - } - }); - } - - onClick(event: MouseEvent) { - const actionMode = this.actionModeService.getActionMode(); - if (actionMode === ActionMode.None) { - this.layerTimelineService.clearSelections(); - } else if (actionMode === ActionMode.Selection) { - this.actionModeService.setSelections([]); - } else { - this.actionModeService.setActionMode(ActionMode.Selection); - } - } - - isMobile() { - return window.navigator.userAgent.includes('Mobile'); - } -} - -interface Size { - readonly w: number; - readonly h: number; -} - -function getUrlParameter(name: string) { - name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'); - const regex = new RegExp('[\\?&]' + name + '=([^&#]*)'); - const results = regex.exec(location.search); - return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' ')); -} diff --git a/src/app/pages/editor/components/scrollgroup/scrollgroup.directive.ts b/src/app/pages/editor/components/scrollgroup/scrollgroup.directive.ts deleted file mode 100644 index 90fcc1d4..00000000 --- a/src/app/pages/editor/components/scrollgroup/scrollgroup.directive.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Directive, ElementRef, HostListener, Input, OnDestroy } from '@angular/core'; -import * as $ from 'jquery'; - -const GROUPS = new Map(); - -@Directive({ - selector: '[appScrollGroup]', -}) -export class ScrollGroupDirective implements OnDestroy { - @Input() - scrollGroup: string; - - private readonly element: JQuery; - - constructor(elementRef: ElementRef) { - this.element = $(elementRef.nativeElement); - GROUPS.set(this.scrollGroup, GROUPS.get(this.scrollGroup) || []); - GROUPS.get(this.scrollGroup).push(this.element); - } - - ngOnDestroy() { - GROUPS.get(this.scrollGroup).splice(GROUPS.get(this.scrollGroup).indexOf(this.element), 1); - } - - @HostListener('scroll', ['$event']) - onScrollEvent(event: MouseEvent) { - const scrollTop = this.element.scrollTop(); - GROUPS.get(this.scrollGroup).forEach(e => { - if (this.element !== e) { - e.scrollTop(scrollTop); - } - }); - } -} diff --git a/src/app/pages/editor/components/splashscreen/splashscreen.component.html b/src/app/pages/editor/components/splashscreen/splashscreen.component.html deleted file mode 100644 index 63efb0e3..00000000 --- a/src/app/pages/editor/components/splashscreen/splashscreen.component.html +++ /dev/null @@ -1,10 +0,0 @@ -
- - - -
Shape Shifter is an icon animation tool designed for desktop browsers
- -
diff --git a/src/app/pages/editor/components/splashscreen/splashscreen.component.scss b/src/app/pages/editor/components/splashscreen/splashscreen.component.scss deleted file mode 100644 index 4e5a76da..00000000 --- a/src/app/pages/editor/components/splashscreen/splashscreen.component.scss +++ /dev/null @@ -1,25 +0,0 @@ -@import '~@angular/material/theming'; -:host { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - user-select: none; - background-color: mat-color(mat-palette($mat-grey, 300)); -} - -.splashscreen-logo { - width: 144px; - height: 144px; - margin-bottom: 16px; -} - -.splashscreen-text { - font-size: 18px; - line-height: 22px; - padding-left: 16px; - padding-right: 16px; - padding-bottom: 16px; - text-align: center; -} diff --git a/src/app/pages/editor/components/splashscreen/splashscreen.component.ts b/src/app/pages/editor/components/splashscreen/splashscreen.component.ts deleted file mode 100644 index e3808c84..00000000 --- a/src/app/pages/editor/components/splashscreen/splashscreen.component.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core'; - -@Component({ - selector: 'app-splashscreen', - templateUrl: './splashscreen.component.html', - styleUrls: ['./splashscreen.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class SplashScreenComponent {} diff --git a/src/app/pages/editor/components/splitter/splitter.component.html b/src/app/pages/editor/components/splitter/splitter.component.html deleted file mode 100644 index 7c89b545..00000000 --- a/src/app/pages/editor/components/splitter/splitter.component.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/src/app/pages/editor/components/splitter/splitter.component.scss b/src/app/pages/editor/components/splitter/splitter.component.scss deleted file mode 100644 index ac501f31..00000000 --- a/src/app/pages/editor/components/splitter/splitter.component.scss +++ /dev/null @@ -1,41 +0,0 @@ -$splitterWidth: 4px; -:host { - position: absolute; - z-index: 100; -} - -:host(.splt-vertical) { - position: absolute; - z-index: 100; - top: 0; - bottom: 0; - width: $splitterWidth; - cursor: col-resize; -} - -:host(.splt-horizontal) { - position: absolute; - z-index: 100; - left: 0; - right: 0; - height: $splitterWidth; - cursor: row-resize; -} - -:host(.splt-edge-left) { - position: absolute; - z-index: 100; - left: 0; -} - -:host(.splt-edge-right) { - position: absolute; - z-index: 100; - right: 0; -} - -:host(.splt-edge-top) { - position: absolute; - z-index: 100; - top: 0; -} diff --git a/src/app/pages/editor/components/splitter/splitter.component.ts b/src/app/pages/editor/components/splitter/splitter.component.ts deleted file mode 100644 index 77fd1327..00000000 --- a/src/app/pages/editor/components/splitter/splitter.component.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { - Component, - ElementRef, - EventEmitter, - HostBinding, - HostListener, - Input, - OnInit, - Output, -} from '@angular/core'; -import { Dragger } from 'app/pages/editor/scripts/dragger'; -import * as $ from 'jquery'; - -type Orientation = 'vertical' | 'horizontal'; -type Edge = 'left' | 'right' | 'top'; - -@Component({ - selector: 'app-splitter', - templateUrl: './splitter.component.html', - styleUrls: ['./splitter.component.scss'], - // TODO: use 'OnPush' change detection -}) -export class SplitterComponent implements OnInit { - @Input() - edge: Edge; - @Input() - min: number; - @Input() - persistId: string; - @Output() - split = new EventEmitter(); - @HostBinding('class.splt-horizontal') - spltHorizontal: boolean; - @HostBinding('class.splt-vertical') - spltVertical: boolean; - @HostBinding('class.splt-edge-left') - spltEdgeLeft: boolean; - @HostBinding('class.splt-edge-right') - spltEdgeRight: boolean; - @HostBinding('style.backgroundColor') - backgroundColor = ''; - - private persistKey: string; - private orientation: Orientation; - private sizeGetterFn: () => number; - private sizeSetterFn: (size: number) => void; - - private isHovering = false; - private isDragging = false; - - constructor(private readonly elementRef: ElementRef) {} - - ngOnInit() { - if (this.min === undefined || this.min <= 0) { - this.min = 100; - } - if (this.persistId) { - this.persistKey = `\$\$splitter::${this.persistId}`; - } - this.orientation = this.edge === 'left' || this.edge === 'right' ? 'vertical' : 'horizontal'; - this.spltHorizontal = this.orientation === 'horizontal'; - this.spltVertical = this.orientation === 'vertical'; - this.spltEdgeLeft = this.edge === 'left'; - this.spltEdgeRight = this.edge === 'right'; - const getParentFn = () => $(this.elementRef.nativeElement).parent(); - if (this.orientation === 'vertical') { - this.sizeGetterFn = () => getParentFn().width(); - this.sizeSetterFn = size => { - getParentFn().width(size); - this.split.emit(size); - }; - } else { - this.sizeGetterFn = () => getParentFn().height(); - this.sizeSetterFn = size => { - getParentFn().height(size); - this.split.emit(size); - }; - } - if (this.persistKey in localStorage) { - this.setSize(Number(localStorage[this.persistKey])); - } - } - - private setSize(size: number) { - if (this.persistKey) { - localStorage[this.persistKey] = size; - } - this.sizeSetterFn(size); - } - - @HostListener('mousedown', ['$event']) - onMouseDown(event: MouseEvent) { - const downSize = this.sizeGetterFn(); - event.preventDefault(); - - this.isDragging = true; - this.showSplitter(); - - // tslint:disable-next-line: no-unused-expression - new Dragger({ - downX: event.clientX, - downY: event.clientY, - direction: this.orientation === 'vertical' ? 'horizontal' : 'vertical', - draggingCursor: this.orientation === 'vertical' ? 'col-resize' : 'row-resize', - onBeginDragFn: () => { - this.isDragging = true; - this.showSplitter(); - }, - onDragFn: (_, p) => { - const sign = this.edge === 'left' || this.edge === 'top' ? -1 : 1; - const d = this.orientation === 'vertical' ? p.x : p.y; - this.setSize(Math.max(this.min, downSize + sign * d)); - }, - onDropFn: () => { - this.isDragging = false; - this.hideSplitter(); - }, - }); - } - - @HostListener('mouseenter') - onMouseEnter() { - this.isHovering = true; - this.showSplitter(); - } - - @HostListener('mouseleave') - onMouseLeave() { - this.isHovering = false; - this.hideSplitter(); - } - - private showSplitter() { - if (this.isDragging || this.isHovering) { - this.backgroundColor = 'rgba(0, 0, 0, 0.1)'; - } - } - - private hideSplitter() { - if (!this.isDragging && !this.isHovering) { - this.backgroundColor = 'transparent'; - } - } -} diff --git a/src/app/pages/editor/components/toolbar/_toolbar-theme.scss b/src/app/pages/editor/components/toolbar/_toolbar-theme.scss deleted file mode 100644 index b6dd9402..00000000 --- a/src/app/pages/editor/components/toolbar/_toolbar-theme.scss +++ /dev/null @@ -1,22 +0,0 @@ -@mixin ss-toolbar-theme($theme) { - $primary: map-get($theme, primary); - $foreground: map-get($theme, ss-foreground); - $accent-inverse: map-get($theme, accent-inverse); - $is-dark: map-get($theme, is-dark); - .toolbar { - background-color: mat-color($primary); - color: mat-color($primary, default-contrast); - button { - &[mat-icon-button][disabled] { - mat-icon { - color: mat-color($foreground, if($is-dark, disabled-text, disabled-text-inverse)); - } - } - } - button.activated { - mat-icon { - color: mat-color($accent-inverse); - } - } - } -} diff --git a/src/app/pages/editor/components/toolbar/toolbar.component.html b/src/app/pages/editor/components/toolbar/toolbar.component.html deleted file mode 100644 index c7854369..00000000 --- a/src/app/pages/editor/components/toolbar/toolbar.component.html +++ /dev/null @@ -1,223 +0,0 @@ - -
- - - - - - - - - - -
- {{ data.toolbarTitle }} - - {{ data.toolbarSubtitle }} - -
- - - - - - - - - - - - - - - - - - - - - - - - - -
- - Dark theme -
- - - - - - - - - -
-
-
-
diff --git a/src/app/pages/editor/components/toolbar/toolbar.component.scss b/src/app/pages/editor/components/toolbar/toolbar.component.scss deleted file mode 100644 index 1a2d1cfd..00000000 --- a/src/app/pages/editor/components/toolbar/toolbar.component.scss +++ /dev/null @@ -1,42 +0,0 @@ -.toolbar { - padding: 8px 16px; - z-index: 1; - .toolbar-logo { - width: 40px; - height: 40px; - margin-right: 8px; - } - .toolbar-title { - font-size: 18px; - } - .toolbar-subtitle { - margin-top: 4px; - font-size: 13px; - } -} - -.toolbar-overflow-link-item { - // Remove link decorations from overflow menu items. - color: inherit; - text-decoration: none; - cursor: inherit; -} - -.action-mode-close-icon { - margin-right: 8px; -} - -.toolbar-action-button { - margin-left: 16px; -} - -a { - button.mat-menu-item { - mat-icon { - margin-right: 20px; - } - } -} -mat-slide-toggle { - margin-right: 8px; -} diff --git a/src/app/pages/editor/components/toolbar/toolbar.component.ts b/src/app/pages/editor/components/toolbar/toolbar.component.ts deleted file mode 100644 index 32afc1c5..00000000 --- a/src/app/pages/editor/components/toolbar/toolbar.component.ts +++ /dev/null @@ -1,386 +0,0 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { - ActionMode, - ActionSource, - Selection, - SelectionType, -} from 'app/pages/editor/model/actionmode'; -import { MorphableLayer } from 'app/pages/editor/model/layers'; -import { PathAnimationBlock } from 'app/pages/editor/model/timeline'; -import { ActionModeUtil } from 'app/pages/editor/scripts/actionmode'; -import { ActionModeService, ThemeService } from 'app/pages/editor/services'; -import { State, Store } from 'app/pages/editor/store'; -import { getToolbarState } from 'app/pages/editor/store/actionmode/selectors'; -import { ThemeType } from 'app/pages/editor/store/theme/reducer'; -import * as _ from 'lodash'; -import { Observable, combineLatest } from 'rxjs'; -import { map } from 'rxjs/operators'; - -declare const ga: Function; - -@Component({ - selector: 'app-toolbar', - templateUrl: './toolbar.component.html', - styleUrls: ['./toolbar.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ToolbarComponent implements OnInit { - toolbarData$: Observable; - themeState$: Observable<{ - prevThemeType: ThemeType; - currThemeType: ThemeType; - prevIsActionMode: boolean; - currIsActionMode: boolean; - }>; - - constructor( - private readonly actionModeService: ActionModeService, - readonly themeService: ThemeService, - private readonly store: Store, - ) {} - - ngOnInit() { - let hasActionModeBeenEnabled = false; - let prevThemeType: ThemeType; - let currThemeType = this.themeService.getThemeType().themeType; - let prevIsActionMode: boolean; - let currIsActionMode = this.actionModeService.getActionMode() !== ActionMode.None; - const toolbarState = this.store.select(getToolbarState); - this.toolbarData$ = toolbarState.pipe( - map(({ mode, fromMl, toMl, selections, unpairedSubPath, block }) => { - return new ToolbarData(mode, fromMl, toMl, selections, unpairedSubPath, block); - }), - ); - this.themeState$ = combineLatest( - toolbarState, - this.themeService.asObservable().pipe(map(t => t.themeType)), - ).pipe( - map(([{ mode }, themeType]) => { - hasActionModeBeenEnabled = hasActionModeBeenEnabled || mode !== ActionMode.None; - prevThemeType = currThemeType; - currThemeType = themeType; - prevIsActionMode = currIsActionMode; - currIsActionMode = mode !== ActionMode.None; - return { - hasActionModeBeenEnabled, - prevThemeType, - currThemeType, - prevIsActionMode, - currIsActionMode, - }; - }), - ); - } - - get darkTheme() { - return this.themeService.getThemeType().themeType === 'dark'; - } - - set darkTheme(isDark: boolean) { - this.themeService.setTheme(isDark ? 'dark' : 'light'); - } - - onSendFeedbackClick(event: MouseEvent) { - ga('send', 'event', 'Miscellaneous', 'Send feedback click'); - } - - onContributeClick(event: MouseEvent) { - ga('send', 'event', 'Miscellaneous', 'Contribute click'); - } - - onGettingStartedClick(event: MouseEvent) { - ga('send', 'event', 'Miscellaneous', 'Getting started click'); - } - - onAutoFixClick(event: MouseEvent) { - ga('send', 'event', 'Action mode', 'Auto fix click'); - event.stopPropagation(); - this.actionModeService.autoFix(); - } - - onCloseActionModeClick(event: MouseEvent) { - event.stopPropagation(); - this.actionModeService.closeActionMode(); - } - - onAddPointsClick(event: MouseEvent) { - ga('send', 'event', 'Action mode', 'Add points'); - event.stopPropagation(); - this.actionModeService.toggleSplitCommandsMode(); - } - - onSplitSubPathsClick(event: MouseEvent) { - ga('send', 'event', 'Action mode', 'Split sub paths'); - event.stopPropagation(); - this.actionModeService.toggleSplitSubPathsMode(); - } - - onPairSubPathsClick(event: MouseEvent) { - ga('send', 'event', 'Action mode', 'Pair sub paths'); - event.stopPropagation(); - this.actionModeService.togglePairSubPathsMode(); - } - - onReversePointsClick(event: MouseEvent) { - event.stopPropagation(); - this.actionModeService.reverseSelectedSubPaths(); - } - - onShiftBackPointsClick(event: MouseEvent) { - event.stopPropagation(); - this.actionModeService.shiftBackSelectedSubPaths(); - } - - onShiftForwardPointsClick(event: MouseEvent) { - event.stopPropagation(); - this.actionModeService.shiftForwardSelectedSubPaths(); - } - - onDeleteSubPathsClick(event: MouseEvent) { - event.stopPropagation(); - this.actionModeService.deleteSelectedActionModeModels(); - } - - onDeleteSegmentsClick(event: MouseEvent) { - event.stopPropagation(); - this.actionModeService.deleteSelectedActionModeModels(); - } - - onSetFirstPositionClick(event: MouseEvent) { - event.stopPropagation(); - this.actionModeService.shiftPointToFront(); - } - - onSplitInHalfHoverEvent(isHovering: boolean) { - if (isHovering) { - this.actionModeService.splitInHalfHover(); - } else { - this.actionModeService.clearHover(); - } - } - - onSplitInHalfClick(event: MouseEvent) { - event.stopPropagation(); - this.actionModeService.splitSelectedPointInHalf(); - } - - onDeletePointsClick(event: MouseEvent) { - event.stopPropagation(); - this.actionModeService.deleteSelectedActionModeModels(); - } -} - -class ToolbarData { - private readonly subPaths: ReadonlyArray = []; - private readonly segments: ReadonlyArray<{ subIdx: number; cmdIdx: number }> = []; - private readonly points: ReadonlyArray<{ subIdx: number; cmdIdx: number }> = []; - private readonly numSplitSubPaths: number; - private readonly numSplitPoints: number; - private readonly showSetFirstPosition: boolean; - private readonly showShiftSubPath: boolean; - private readonly isFilled: boolean; - private readonly isStroked: boolean; - private readonly showSplitInHalf: boolean; - private readonly unpairedSubPathSource: ActionSource; - private readonly showPairSubPaths: boolean; - - constructor( - readonly mode: ActionMode, - startMorphableLayer: MorphableLayer, - endMorphableLayer: MorphableLayer, - readonly selections: ReadonlyArray, - unpair: { source: ActionSource; subIdx: number }, - private readonly block: PathAnimationBlock | undefined, - ) { - // Precondition: assume all selections are for the same canvas type - if (!selections.length) { - return; - } - const canvasType = selections[0].source; - const morphableLayer = - canvasType === ActionSource.From ? startMorphableLayer : endMorphableLayer; - if (!morphableLayer) { - return; - } - const activePath = morphableLayer.pathData; - this.isFilled = morphableLayer.isFilled(); - this.isStroked = morphableLayer.isStroked(); - this.subPaths = selections.filter(s => s.type === SelectionType.SubPath).map(s => s.subIdx); - this.segments = selections - .filter(s => { - const { subIdx, cmdIdx } = s; - return ( - s.type === SelectionType.Segment && - morphableLayer.isFilled() && - activePath.getCommand(subIdx, cmdIdx).isSplitSegment() - ); - }) - .map(s => { - const { subIdx, cmdIdx } = s; - return { subIdx, cmdIdx }; - }); - this.points = selections.filter(s => s.type === SelectionType.Point).map(s => { - const { subIdx, cmdIdx } = s; - return { subIdx, cmdIdx }; - }); - - this.numSplitSubPaths = _.sumBy(this.subPaths, subIdx => { - return activePath.getSubPath(subIdx).isUnsplittable() ? 1 : 0; - }); - this.numSplitPoints = _.sumBy(this.points, s => { - const { subIdx, cmdIdx } = s; - return activePath.getCommand(subIdx, cmdIdx).isSplitPoint() ? 1 : 0; - }); - this.showSetFirstPosition = - this.points.length === 1 && - this.points[0].cmdIdx && - activePath.getSubPath(this.points[0].subIdx).isClosed(); - this.showShiftSubPath = - this.subPaths.length > 0 && activePath.getSubPath(this.subPaths[0]).isClosed(); - this.showSplitInHalf = this.points.length === 1 && !!this.points[0].cmdIdx; - if (this.mode === ActionMode.PairSubPaths) { - if (unpair) { - this.unpairedSubPathSource = unpair.source; - } - } - this.showPairSubPaths = - startMorphableLayer.pathData.getSubPaths().length === 1 && - endMorphableLayer.pathData.getSubPaths().length === 1 - ? false - : this.getNumSubPaths() === 1 || this.getNumSegments() > 0 || !this.isSelectionMode(); - } - - getNumSelections() { - return this.subPaths.length + this.segments.length + this.points.length; - } - - getNumSubPaths() { - return this.subPaths.length; - } - - getNumSegments() { - return this.segments.length; - } - - getNumPoints() { - return this.points.length; - } - - getToolbarTitle() { - if (this.mode === ActionMode.SplitCommands) { - return 'Add points'; - } - if (this.mode === ActionMode.SplitSubPaths) { - return 'Split subpaths'; - } - if (this.mode === ActionMode.PairSubPaths) { - return 'Pair subpaths'; - } - const numSubPaths = this.getNumSubPaths(); - const subStr = `${numSubPaths} subpath${numSubPaths === 1 ? '' : 's'}`; - const numSegments = this.getNumSegments(); - const segStr = `${numSegments} segment${numSegments === 1 ? '' : 's'}`; - const numPoints = this.getNumPoints(); - const ptStr = `${numPoints} point${numPoints === 1 ? '' : 's'}`; - if (numSubPaths > 0) { - return `${subStr} selected`; - } else if (numSegments > 0) { - return `${segStr} selected`; - } else if (numPoints > 0) { - return `${ptStr} selected`; - } else if (this.mode === ActionMode.Selection) { - return 'Edit path morphing animation'; - } - return 'Shape Shifter'; - } - - getToolbarSubtitle() { - if (this.mode === ActionMode.SplitCommands) { - return 'Click along the edge of a subpath to add a point'; - } else if (this.mode === ActionMode.SplitSubPaths) { - if (this.isFilled) { - return 'Draw a line across a subpath to split it into 2'; - } else if (this.isStroked) { - return 'Click along the edge of a subpath to split it into 2'; - } - } else if (this.mode === ActionMode.PairSubPaths) { - if (this.unpairedSubPathSource) { - const toSourceDir = this.unpairedSubPathSource === ActionSource.From ? 'right' : 'left'; - return `Pair the selected subpath with a corresponding subpath on the ${toSourceDir}`; - } - return 'Select a subpath'; - } else if (this.mode === ActionMode.Selection) { - const { areCompatible, errorPath, numPointsMissing } = ActionModeUtil.checkPathsCompatible( - this.block, - ); - if (!areCompatible) { - const createSubtitleFn = (direction: string) => { - if (numPointsMissing === 1) { - return `Add 1 point to the highlighted subpath on the ${direction}`; - } else { - return `Add ${numPointsMissing} points to the highlighted subpath on the ${direction}`; - } - }; - if (errorPath === ActionSource.From) { - return createSubtitleFn('left'); - } else if (errorPath === ActionSource.To) { - return createSubtitleFn('right'); - } - // This should never happen, but return empty string just to be safe. - return ''; - } - if (!this.getNumSubPaths() && !this.getNumSegments() && !this.getNumPoints()) { - return 'Select something below to edit its properties'; - } - } - return ''; - } - - shouldShowActionMode() { - return this.mode !== ActionMode.None; - } - - shouldShowPairSubPaths() { - return this.showPairSubPaths; - } - - getNumSplitSubPaths() { - return this.numSplitSubPaths || 0; - } - - getNumSplitPoints() { - return this.numSplitPoints || 0; - } - - shouldShowSetFirstPosition() { - return this.showSetFirstPosition || false; - } - - shouldShowShiftSubPath() { - return this.showShiftSubPath || false; - } - - shouldShowSplitInHalf() { - return this.showSplitInHalf || false; - } - - isSelectionMode() { - return this.mode === ActionMode.None || this.mode === ActionMode.Selection; - } - - isAddPointsMode() { - return this.mode === ActionMode.SplitCommands; - } - - isSplitSubPathsMode() { - return this.mode === ActionMode.SplitSubPaths; - } - - isPairSubPathsMode() { - return this.mode === ActionMode.PairSubPaths; - } - - shouldShowAutoFix() { - return this.mode === ActionMode.Selection && !this.getNumSelections(); - } -} diff --git a/src/app/pages/editor/components/toolpanel/toolpanel.component.html b/src/app/pages/editor/components/toolpanel/toolpanel.component.html deleted file mode 100644 index 20501de2..00000000 --- a/src/app/pages/editor/components/toolpanel/toolpanel.component.html +++ /dev/null @@ -1,71 +0,0 @@ -
- - - - - - - - -
\ No newline at end of file diff --git a/src/app/pages/editor/components/toolpanel/toolpanel.component.scss b/src/app/pages/editor/components/toolpanel/toolpanel.component.scss deleted file mode 100644 index cc593ce7..00000000 --- a/src/app/pages/editor/components/toolpanel/toolpanel.component.scss +++ /dev/null @@ -1,17 +0,0 @@ -.tool-panel { - // TODO: move this into a themed CSS file - background-color: #eeeeee; - position: relative; - - .tool-button { - margin: 4px; - &:hover { - cursor: pointer; - } - &.is-checked { - // TODO: move this into a themed CSS file - background-color: #d0d0d0; - } - } -} - diff --git a/src/app/pages/editor/components/toolpanel/toolpanel.component.ts b/src/app/pages/editor/components/toolpanel/toolpanel.component.ts deleted file mode 100644 index 9c46fb94..00000000 --- a/src/app/pages/editor/components/toolpanel/toolpanel.component.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { ToolMode } from 'app/pages/editor/model/paper'; -import { PaperService } from 'app/pages/editor/services'; -import { Observable } from 'rxjs'; - -@Component({ - selector: 'app-toolpanel', - templateUrl: './toolpanel.component.html', - styleUrls: ['./toolpanel.component.scss'], -}) -export class ToolPanelComponent implements OnInit { - readonly TOOL_MODE_PENCIL = ToolMode.Pencil; - readonly TOOL_MODE_ELLIPSE = ToolMode.Ellipse; - readonly TOOL_MODE_RECTANGLE = ToolMode.Rectangle; - readonly TOOL_MODE_ZOOMPAN = ToolMode.ZoomPan; - - // TODO: only enable edit path/rotate/transform in default mode? - model$: Observable; - - constructor(private readonly ps: PaperService) {} - - ngOnInit() { - this.model$ = this.ps.observeToolPanelState(); - } - - onDefaultClick(event: Event) { - this.ps.enterDefaultMode(); - event.stopPropagation(); - } - - onRotateItemsClick(event: Event) { - this.ps.enterRotateItemsMode(); - event.stopPropagation(); - } - - onTransformPathsClick(event: Event) { - this.ps.enterTransformPathsMode(); - event.stopPropagation(); - } - - onPencilClick(event: Event) { - this.ps.enterPencilMode(); - event.stopPropagation(); - } - - onEditPathClick(event: Event) { - this.ps.enterEditPathMode(); - event.stopPropagation(); - } - - onEllipseClick(event: Event) { - this.ps.enterCreateEllipseMode(); - event.stopPropagation(); - } - - onRectangleClick(event: Event) { - this.ps.enterCreateRectangleMode(); - event.stopPropagation(); - } - - onZoomPanClick(event: Event) { - this.ps.setToolMode(ToolMode.ZoomPan); - event.stopPropagation(); - } -} - -interface ToolPanelModel { - readonly toolMode: ToolMode; - readonly isDefaultChecked: boolean; - readonly isEditPathChecked: boolean; - readonly isRotateItemsEnabled: boolean; - readonly isRotateItemsChecked: boolean; - readonly isTransformPathsEnabled: boolean; - readonly isTransformPathsChecked: boolean; -} diff --git a/src/app/pages/editor/editor.module.ts b/src/app/pages/editor/editor.module.ts deleted file mode 100644 index 9dd4aff4..00000000 --- a/src/app/pages/editor/editor.module.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { HttpClientModule } from '@angular/common/http'; -import { ErrorHandler, NgModule } from '@angular/core'; -import { FlexLayoutModule } from '@angular/flex-layout'; -import { FormsModule } from '@angular/forms'; -import { - MatButtonModule, - MatDialogModule, - MatIconModule, - MatIconRegistry, - MatInputModule, - MatMenuModule, - MatOptionModule, - MatRadioModule, - MatSlideToggleModule, - MatSnackBarModule, - MatToolbarModule, - MatTooltipModule, -} from '@angular/material'; -import { BrowserModule, DomSanitizer } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ServiceWorkerModule } from '@angular/service-worker'; -import { - CanvasComponent, - CanvasContainerDirective, - CanvasLayersDirective, - CanvasOverlayDirective, - CanvasPaperDirective, - CanvasRulerDirective, -} from 'app/pages/editor/components/canvas'; -import { - ConfirmDialogComponent, - DemoDialogComponent, - DropFilesDialogComponent, -} from 'app/pages/editor/components/dialogs'; -import { - LayerListTreeComponent, - LayerTimelineComponent, - LayerTimelineGridDirective, - TimelineAnimationRowComponent, -} from 'app/pages/editor/components/layertimeline'; -import { PlaybackComponent } from 'app/pages/editor/components/playback'; -import { PropertyInputComponent } from 'app/pages/editor/components/propertyinput'; -import { DropTargetDirective } from 'app/pages/editor/components/root/droptarget.directive'; -import { RootComponent } from 'app/pages/editor/components/root/root.component'; -import { ScrollGroupDirective } from 'app/pages/editor/components/scrollgroup/scrollgroup.directive'; -import { SplashScreenComponent } from 'app/pages/editor/components/splashscreen/splashscreen.component'; -import { SplitterComponent } from 'app/pages/editor/components/splitter/splitter.component'; -import { ToolbarComponent } from 'app/pages/editor/components/toolbar/toolbar.component'; -import { ToolPanelComponent } from 'app/pages/editor/components/toolpanel/toolpanel.component'; -import { errorHandlerFactory } from 'app/pages/editor/scripts/bugsnag'; -import { StoreModule, metaReducers, reducers } from 'app/pages/editor/store'; -import { environment } from 'environments/environment'; - -@NgModule({ - declarations: [ - CanvasComponent, - CanvasContainerDirective, - CanvasLayersDirective, - CanvasOverlayDirective, - CanvasPaperDirective, - CanvasRulerDirective, - ConfirmDialogComponent, - DemoDialogComponent, - DropFilesDialogComponent, - DropTargetDirective, - LayerListTreeComponent, - LayerTimelineComponent, - LayerTimelineGridDirective, - PlaybackComponent, - PropertyInputComponent, - RootComponent, - ScrollGroupDirective, - SplashScreenComponent, - SplitterComponent, - TimelineAnimationRowComponent, - ToolbarComponent, - ToolPanelComponent, - ], - imports: [ - BrowserModule, - BrowserAnimationsModule, - FlexLayoutModule, - FormsModule, - HttpClientModule, - ServiceWorkerModule.register('/ngsw-worker.js', { enabled: environment.production }), - StoreModule.forRoot(reducers, { metaReducers }), - // Angular material components. - MatButtonModule, - MatDialogModule, - MatIconModule, - MatInputModule, - MatMenuModule, - MatOptionModule, - MatRadioModule, - MatSlideToggleModule, - MatSnackBarModule, - MatToolbarModule, - MatTooltipModule, - ], - providers: [{ provide: ErrorHandler, useFactory: errorHandlerFactory }], - entryComponents: [ConfirmDialogComponent, DemoDialogComponent, DropFilesDialogComponent], - bootstrap: [RootComponent], -}) -export class EditorModule { - constructor(matIconRegistry: MatIconRegistry, private readonly sanitizer: DomSanitizer) { - matIconRegistry - // Logo. - .addSvgIcon('shapeshifter', this.trustUrl('assets/shapeshifter.svg')) - // Icons. - .addSvgIcon('addlayer', this.trustUrl('assets/icons/addlayer.svg')) - .addSvgIcon('autofix', this.trustUrl('assets/icons/autofix.svg')) - .addSvgIcon('contribute', this.trustUrl('assets/icons/contribute.svg')) - .addSvgIcon('reverse', this.trustUrl('assets/icons/reverse.svg')) - .addSvgIcon('animation', this.trustUrl('assets/icons/animation.svg')) - .addSvgIcon('collection', this.trustUrl('assets/icons/collection.svg')) - .addSvgIcon('animationblock', this.trustUrl('assets/icons/animationblock.svg')) - .addSvgIcon('mask', this.trustUrl('assets/icons/clippathlayer.svg')) - .addSvgIcon('group', this.trustUrl('assets/icons/grouplayer.svg')) - .addSvgIcon('path', this.trustUrl('assets/icons/pathlayer.svg')) - .addSvgIcon('vector', this.trustUrl('assets/icons/vectorlayer.svg')) - // Tools. - .addSvgIcon('tool_select', this.trustUrl('assets/tools/tool_select.svg')) - .addSvgIcon('tool_pencil', this.trustUrl('assets/tools/tool_pencil.svg')) - .addSvgIcon('tool_vector', this.trustUrl('assets/tools/tool_vector.svg')) - .addSvgIcon('tool_ellipse', this.trustUrl('assets/tools/tool_ellipse.svg')) - .addSvgIcon('tool_rectangle', this.trustUrl('assets/tools/tool_rectangle.svg')) - .addSvgIcon('tool_zoompan', this.trustUrl('assets/tools/tool_zoompan.svg')); - } - - private trustUrl(url: string) { - return this.sanitizer.bypassSecurityTrustResourceUrl(url); - } -} diff --git a/src/app/pages/editor/model/README.md b/src/app/pages/editor/model/README.md deleted file mode 100644 index ae25d227..00000000 --- a/src/app/pages/editor/model/README.md +++ /dev/null @@ -1,155 +0,0 @@ -# Shape Shifter model docs - -This document describes the model objects that make up a Shape Shifter project. - -## `Layer` - -Each Shape Shifter project is composed of a tree of `Layer` objects. The `Layer`s that make up this tree are displayed in the bottom left panel of the UI. - -Every `Layer` object has a unique string `id` field. No two layers in a project will ever share the same `id`. - -Every `Layer` type has a set of "inspectable" properties. When the layer is selected, these properties will be listed in the far right rectangular panel of the UI. A subset of these properties may be "animatable", meaning that they can be animated in the timeline at the bottom of the UI. Properties will take on one of the following value types: - -- `integer` - A numeric value expressed as an integer. -- `float` - A numeric value expressed as a decimal. -- `string` - A string containing plain text. -- `color string` - A string representing an ARGB color. May be in one of the following formats: `#RGB`, `#RRGGBB`, or `#AARRGGBB`. -- `path string` - A string representing an SVG path. The contents of the string uses the SVG path data spec notation. -- `enum string` - A string representing an enum, meaning it will take on one of some fixed number of values. - -There are currently four types of `Layer`s (all of which extend an abstract `Layer` base class): - -### `VectorLayer` - -This is the root node of the `Layer` tree and holds a list of 0 or more children `Layer`s. When you create a new Shape Shifter project, the project will consist of just a single empty `VectorLayer` object named `vector`. There will only ever be one `VectorLayer` object in a Shape Shifter project. This `Layer` is similar to the `` node in an SVG and/or the `` node in a `VectorDrawable`. - -#### Properties - -- `name` (string) - A unique name for the layer to be displayed in the UI. - -- `children` (list of `Layer`s) - A list of children `Layer`s. - -- `canvasColor` (color string) - An ARGB hex string describing the canvas background color. This value is currently only used by the Shape Shifter UI (it's not used at all in any of the export options). - -- `width` (integer) - An integer greater than `0` describing the viewport width of the canvas. - -- `height` (integer) - An integer greater than `0` describing the viewport height of the canvas. - -- `alpha` (float, animatable) - A float value in the interval `[0,1]` describing the opacity of the layer tree. Default value is `1`. - -### `GroupLayer` - -A `GroupLayer` defines a group of 0 or more children `Layer`s. It has several properties that allow you to apply transformations on its children `Layer`s as well. Transformations are defined in viewport space (i.e. in terms of the viewport width/height set on the root `VectorLayer` node). Transformations are applied in the order of scale, rotation, and then translation. Similar to the `` node of an SVG and/or the `` node of a `VectorDrawable`. - -#### Properties - -- `name` (string) - A unique name for the layer to be displayed in the UI. - -- `children` (list of `Layer`s) - A list of children `Layer`s. - -- `rotation` (float, animatable) - A float value describing the rotation of the group. Default value is `0`. - -- `scaleX` (float, animatable) - A float value describing the amount to scale in the x-direction. Default value is `1`. - -- `scaleY` (float, animatable) - A float value describing the amount to scale in the y-direction. Default value is `1`. - -- `pivotX` (float, animatable) - A float value (defined in viewport space) describing the x-coordinate of the pivot used to scale/rotate the group. Default value is `0`. - -- `pivotY` (float, animatable) - A float value (defined in viewport space) describing the y-coordinate of the pivot used to scale/rotate the group. Default value is `0`. - -- `translateX` (float, animatable) - A float value (defined in viewport space) describing the amount to translate in the x-direction. Default value is `0`. - -- `translateY` (float, animatable) - A float value (defined in viewport space) describing the amount to translate in the y-direction. Default value is `0`. - -### `PathLayer` - -A `PathLayer` allows us to draw filled and/or stroked shapes to the canvas. Similar to the `` node of an SVG and/or the `` node of a `VectorDrawable`. - -#### Properties - -- `name` (string) - A unique name for the layer to be displayed in the UI. - -- `pathData` (path string, animatable) - A string describing the path's SVG path data. Similar to the `d` attribute of an SVG and/or the `android:pathData` attribute in a `VectorDrawable`. Default value is `undefined`. - -- `fillColor` (color string, animatable) - An ARGB hex string representing the path's fill color. Similar to the `fill` attribute of an SVG and/or the `android:fillColor` attribute in a `VectorDrawable`. Default value is `undefined`. - -- `fillAlpha` (float, animatable) - A float value in the interval `[0,1]` representing the path's fill opacity. Similar to the `fill-opacity` attribute of an SVG and/or the `android:fillAlpha` attribute in a `VectorDrawable`. Default value is `1`. - -- `strokeColor` (color string, animatable) - An ARGB hex string representing the path's stroke color. Similar to the `stroke` attribute of an SVG and/or the `android:strokeColor` attribute in a `VectorDrawable`. Default value is `undefined`. - -- `strokeAlpha` (float, animatable) - A float value in the interval `[0,1]` representing the path's stroke opacity. Similar to the `stroke-opacity` attribute of an SVG and/or the `android:strokeAlpha` attribute in a `VectorDrawable`. Default value is `1`. - -- `strokeWidth` (float, animatable) - A float value greater than or equal to `0` representing the path's stroke width. Similar to the `stroke-width` attribute of an SVG and/or the `android:strokeWidth` attribute in a `VectorDrawable`. Default value is `0`. - -- `strokeLinecap` (string enum) - An enum value of either `butt`, `round`, or `square`. Similar to the `stroke-linecap` attribute of an SVG and/or the `android:strokeLineCap` attribute in a `VectorDrawable`. Default value is `butt`. - -- `strokeLinejoin` (string enum) - An enum value of either `miter`, `round`, or `bevel`. Similar to the `stroke-linejoin` attribute of an SVG and/or the `android:strokeLineJoin` attribute in a `VectorDrawable`. Default value is `miter`. - -- `strokeMiterLimit` (float) - A float value that is greater than or equal to `1` that represents the path's stroke miter limit. Similar to the `stroke-miterlimit` attribute of an SVG and/or the `android:strokeMiterLimit` attribute in a `VectorDrawable`. Default value is `4`. - -- `trimPathStart` - (float, animatable) - A float value in the interval `[0,1]` that represents the path's trim path start value (see this blog post for an in-depth explanation: https://j.mp/icon-animations). Similar to the `android:trimPathStart` attribute in a `VectorDrawable`. Default value is `0`. - -- `trimPathEnd` - (float, animatable) - A float value in the interval `[0,1]` that represents the path's trim path end value (see this blog post for an in-depth explanation: https://j.mp/icon-animations). Similar to the `android:trimPathEnd` attribute in a `VectorDrawable`. Default value is `1`. - -- `trimPathOffset` - (float, animatable) - A float value in the interval `[0,1]` that represents the path's trim path offset value (see this blog post for an in-depth explanation: https://j.mp/icon-animations). Similar to the `android:trimPathOffset` attribute in a `VectorDrawable`. Default value is `0`. - -- `fillType` - (string enum) - An enum value of either `nonZero` or `evenOdd` describing the path's fill type. Similar to the `fill-rule` attribute of an SVG and/or the `android:fillType` attribute in a `VectorDrawable`. Default value is `nonZero`. - -### `ClipPathLayer` - -A `ClipPathLayer` defines an area in which subsequent `Layer`s can be drawn. Note that the clip path only affects its subsequent sibling `Layer`s (i.e. if the `ClipPathLayer` is the 3rd child `Layer` in a `GroupLayer` with 5 total children, then the `ClipPathLayer` will only affect the 4th and 5th child `Layer`s in that group. Similar to the `` node of a `VectorDrawable`. - -#### Properties - -- `name` (string) - A unique name for the layer to be displayed in the UI. - -- `pathData` (path string, animatable) - A string describing the path's SVG path data. Similar to the `d` attribute of an SVG and/or the `android:pathData` attribute in a `VectorDrawable`. Default value is `undefined`. - -## `Animation` - -The `Animation` object contains the information needed to render the timeline at the bottom of the UI. An `Animation` has a unique ID and the following properties: - -### Properties - -- `name` (string) - A unique name for the animation to be displayed in the UI. - -- `duration` (integer) - An integer value in the interval `[100,60000]` representing the duration of the timeline in milliseconds. Default value is `300`. - -- `blocks` (list of `AnimationBlock`s) - A list of animation blocks (discussed below). - -## `AnimationBlock` - -An `AnimationBlock` describes a property animation for a particular `Layer`. They are shown in the animation as rounded rectangular blocks in the timeline UI at the bottom of the screen. - -### Properties - -- `name` (string) - A unique name for the layer to be displayed in the UI. - -- `layerId` (string) - The `id` of the `Layer` that this `AnimationBlock` is associated with. - -- `propertyName` (string) - The name of the `Layer` property this block is animating. - -- `startTime` (integer) - An integer greater than or equal to `0` representing the block's starting time in milliseconds. Default value is `0`. - -- `endTime` (integer) - An integer greater than the block's `startTime` representing the block's ending time in milliseconds. Default value is `100`. - -- `interpolator` (enum string) - Describes the interpolator to use for the property animation. It will be one of the `value`s listed in this [`Interpolator.ts`](https://github.com/alexjlockwood/ShapeShifter/blob/master/src/app/model/interpolators/Interpolator.ts) file. - -- `type` (enum string) - Describes the value type of the associated `Layer` property: `path`, `color`, or `number`. - -- `fromValue` (the value type of the associated `Layer` property) - The start value of the property animation. - -- `toValue` (the value type of the associated `Layer` property) - The end value of the property animation. - -## Useful links - -The source code for each of these model objects is located here: - -- https://github.com/alexjlockwood/ShapeShifter/blob/master/src/app/model/layers/Layer.ts -- https://github.com/alexjlockwood/ShapeShifter/blob/master/src/app/model/timeline/Animation.ts -- https://github.com/alexjlockwood/ShapeShifter/blob/master/src/app/model/timeline/AnimationBlock.ts - -You may also find the documentation for `VectorDrawable` and `AnimatedVectorDrawable` useful, as Shape Shifter was closely modeled after the structure of these two Android classes: - -- https://developer.android.com/reference/android/graphics/drawable/VectorDrawable -- https://developer.android.com/reference/android/graphics/drawable/AnimatedVectorDrawable diff --git a/src/app/pages/editor/model/actionmode/index.ts b/src/app/pages/editor/model/actionmode/index.ts deleted file mode 100644 index 19f44967..00000000 --- a/src/app/pages/editor/model/actionmode/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ActionMode, ActionSource, Hover, HoverType, Selection, SelectionType } from './types'; diff --git a/src/app/pages/editor/model/actionmode/types.ts b/src/app/pages/editor/model/actionmode/types.ts deleted file mode 100644 index 88e0fb27..00000000 --- a/src/app/pages/editor/model/actionmode/types.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * A selection represents an action that is the result of a mouse click. - */ -export interface Selection { - readonly type: SelectionType; - readonly source: ActionSource; - readonly subIdx: number; - readonly cmdIdx?: number; -} - -/** - * Describes the different types of selection events. - */ -export enum SelectionType { - // The user selected an entire subpath. - SubPath = 1, - // The user selected an individual segment in a subpath. - Segment, - // The user selected an individual point in a subpath. - Point, -} - -/** - * Different shape shifter modes. - */ -export enum ActionMode { - None = 1, - Selection, - SplitCommands, - PairSubPaths, - SplitSubPaths, -} - -/** - * Different action sources. - */ -export enum ActionSource { - From = 1, - Animated, - To, -} - -/** - * A hover represents a transient action that results from a mouse movement. - */ -export interface Hover { - readonly type: HoverType; - readonly source: ActionSource; - readonly subIdx: number; - readonly cmdIdx?: number; -} - -/** - * Describes the different types of hover events. - */ -export enum HoverType { - SubPath = 1, - Segment, - Point, - Split, - Unsplit, - Reverse, - ShiftBack, - ShiftForward, - SetFirstPosition, -} diff --git a/src/app/pages/editor/model/interpolators/BezierEasing.ts b/src/app/pages/editor/model/interpolators/BezierEasing.ts deleted file mode 100644 index 103c89ac..00000000 --- a/src/app/pages/editor/model/interpolators/BezierEasing.ts +++ /dev/null @@ -1,113 +0,0 @@ -const NEWTON_ITERATIONS = 4; -const NEWTON_MIN_SLOPE = 1e-3; -const SUBDIVISION_PRECISION = 1e-7; -const SUBDIVISION_MAX_ITERATIONS = 10; -const kSplineTableSize = 11; -const kSampleStepSize = 1 / (kSplineTableSize - 1); -const isFloat32ArraySupported = typeof Float32Array === 'function'; - -function A(aA1: number, aA2: number) { - return 1 - 3 * aA2 + 3 * aA1; -} - -function B(aA1: number, aA2: number) { - return 3 * aA2 - 6 * aA1; -} - -function C(aA1: number) { - return 3 * aA1; -} - -/** Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. */ -function calcBezier(aT: number, aA1: number, aA2: number) { - return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; -} - -/** Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. */ -function getSlope(aT: number, aA1: number, aA2: number) { - return 3 * A(aA1, aA2) * aT * aT + 2 * B(aA1, aA2) * aT + C(aA1); -} - -function binarySubdivide(aX: number, aA: number, aB: number, mX1: number, mX2: number) { - let currentX; - let currentT; - let i = 0; - do { - currentT = aA + (aB - aA) / 2; - currentX = calcBezier(currentT, mX1, mX2) - aX; - if (currentX > 0) { - aB = currentT; - } else { - aA = currentT; - } - } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); - return currentT; -} - -function newtonRaphsonIterate(aX: number, aGuessT: number, mX1: number, mX2: number) { - for (let i = 0; i < NEWTON_ITERATIONS; i++) { - const currentSlope = getSlope(aGuessT, mX1, mX2); - if (currentSlope === 0) { - return aGuessT; - } - const currentX = calcBezier(aGuessT, mX1, mX2) - aX; - aGuessT -= currentX / currentSlope; - } - return aGuessT; -} - -export function create(mX1: number, mY1: number, mX2: number, mY2: number) { - if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { - throw new Error('bezier x values must be in [0, 1] range'); - } - - // Precompute samples table - const sampleValues = isFloat32ArraySupported - ? new Float32Array(kSplineTableSize) - : new Array(kSplineTableSize); - if (mX1 !== mY1 || mX2 !== mY2) { - for (let i = 0; i < kSplineTableSize; i++) { - sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); - } - } - - function getTForX(aX: number) { - let intervalStart = 0; - let currentSample = 1; - const lastSample = kSplineTableSize - 1; - - for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; currentSample++) { - intervalStart += kSampleStepSize; - } - currentSample--; - - // Interpolate to provide an initial guess for t - const dist = - (aX - sampleValues[currentSample]) / - (sampleValues[currentSample + 1] - sampleValues[currentSample]); - const guessForT = intervalStart + dist * kSampleStepSize; - - const initialSlope = getSlope(guessForT, mX1, mX2); - if (initialSlope >= NEWTON_MIN_SLOPE) { - return newtonRaphsonIterate(aX, guessForT, mX1, mX2); - } else if (initialSlope === 0) { - return guessForT; - } else { - return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2); - } - } - - return (x: number) => { - if (mX1 === mY1 && mX2 === mY2) { - return x; // Linear. - } - // Because JavaScript number are imprecise, we should guarantee the extremes are right. - if (x === 0) { - return 0; - } - if (x === 1) { - return 1; - } - return calcBezier(getTForX(x), mY1, mY2); - }; -} diff --git a/src/app/pages/editor/model/interpolators/Interpolator.ts b/src/app/pages/editor/model/interpolators/Interpolator.ts deleted file mode 100644 index 52ab3885..00000000 --- a/src/app/pages/editor/model/interpolators/Interpolator.ts +++ /dev/null @@ -1,118 +0,0 @@ -import * as BezierEasing from './BezierEasing'; - -export interface Interpolator { - readonly value: string; - readonly label: string; - readonly interpolateFn: (fraction: number) => number; - readonly androidRef: string; - readonly webRef: string; -} - -const FAST_OUT_SLOW_IN_EASING = BezierEasing.create(0.4, 0, 0.2, 1); -const FAST_OUT_LINEAR_IN_EASING = BezierEasing.create(0.4, 0, 1, 1); -const LINEAR_OUT_SLOW_IN_EASING = BezierEasing.create(0, 0, 0.2, 1); - -export const INTERPOLATORS: ReadonlyArray = [ - { - value: 'FAST_OUT_SLOW_IN', - label: 'Fast out, slow in', - androidRef: '@android:interpolator/fast_out_slow_in', - interpolateFn: f => FAST_OUT_SLOW_IN_EASING(f), - webRef: 'cubic-bezier(0.4, 0, 0.2, 1)', - }, - { - value: 'FAST_OUT_LINEAR_IN', - label: 'Fast out, linear in', - androidRef: '@android:interpolator/fast_out_linear_in', - interpolateFn: f => FAST_OUT_LINEAR_IN_EASING(f), - webRef: 'cubic-bezier(0.4, 0, 1, 1)', - }, - { - value: 'LINEAR_OUT_SLOW_IN', - label: 'Linear out, slow in', - androidRef: '@android:interpolator/linear_out_slow_in', - interpolateFn: f => LINEAR_OUT_SLOW_IN_EASING(f), - webRef: 'cubic-bezier(0, 0, 0.2, 1)', - }, - { - value: 'ACCELERATE_DECELERATE', - label: 'Accelerate/decelerate', - androidRef: '@android:anim/accelerate_decelerate_interpolator', - interpolateFn: f => Math.cos((f + 1) * Math.PI) / 2.0 + 0.5, - webRef: 'cubic-bezier(0.455, 0.03, 0.515, 0.955)', - }, - { - value: 'ACCELERATE', - label: 'Accelerate', - androidRef: '@android:anim/accelerate_interpolator', - interpolateFn: f => f * f, - webRef: 'cubic-bezier(0.55, 0.085, 0.68, 0.53)', - }, - { - value: 'DECELERATE', - label: 'Decelerate', - androidRef: '@android:anim/decelerate_interpolator', - interpolateFn: f => 1 - (1 - f) * (1 - f), - webRef: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', - }, - { - value: 'LINEAR', - label: 'Linear', - androidRef: '@android:anim/linear_interpolator', - interpolateFn: f => f, - webRef: 'linear', - }, - { - value: 'ANTICIPATE', - label: 'Anticipate', - androidRef: '@android:anim/anticipate_interpolator', - interpolateFn: f => f * f * ((2 + 1) * f - 2), - webRef: 'cubic-bezier(0.4, 0, 0.2, 1)', // TODO: support exporting this interpolator! - }, - { - value: 'OVERSHOOT', - label: 'Overshoot', - androidRef: '@android:anim/overshoot_interpolator', - interpolateFn: f => (f - 1) * (f - 1) * ((2 + 1) * (f - 1) + 2) + 1, - webRef: 'cubic-bezier(0.4, 0, 0.2, 1)', // TODO: support exporting this interpolator! - }, - { - value: 'BOUNCE', - label: 'Bounce', - androidRef: '@android:anim/bounce_interpolator', - interpolateFn: f => { - const bounceFn = (t: number) => t * t * 8; - f *= 1.1226; - if (f < 0.3535) { - return bounceFn(f); - } else if (f < 0.7408) { - return bounceFn(f - 0.54719) + 0.7; - } else if (f < 0.9644) { - return bounceFn(f - 0.8526) + 0.9; - } else { - return bounceFn(f - 1.0435) + 0.95; - } - }, - webRef: 'cubic-bezier(0.4, 0, 0.2, 1)', // TODO: support exporting this interpolator! - }, - { - value: 'ANTICIPATE_OVERSHOOT', - label: 'Anticipate overshoot', - androidRef: '@android:anim/anticipate_overshoot_interpolator', - interpolateFn: f => { - const a = (t: number, s: number) => { - return t * t * ((s + 1) * t - s); - }; - const o = (t: number, s: number) => { - return t * t * ((s + 1) * t + s); - }; - if (f < 0.5) { - return 0.5 * a(f * 2, 2 * 1.5); - } else { - return 0.5 * (o(f * 2 - 2, 2 * 1.5) + 2); - } - }, - webRef: 'cubic-bezier(0.4, 0, 0.2, 1)', // TODO: support exporting this interpolator! - }, - // TODO: add support for custom path interpolators -]; diff --git a/src/app/pages/editor/model/interpolators/index.ts b/src/app/pages/editor/model/interpolators/index.ts deleted file mode 100644 index c306fa4a..00000000 --- a/src/app/pages/editor/model/interpolators/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Interpolator, INTERPOLATORS } from './Interpolator'; diff --git a/src/app/pages/editor/model/layers/Layer.ts b/src/app/pages/editor/model/layers/Layer.ts deleted file mode 100644 index c8c70df0..00000000 --- a/src/app/pages/editor/model/layers/Layer.ts +++ /dev/null @@ -1,552 +0,0 @@ -import { Path } from 'app/pages/editor/model/paths'; -import { - Animatable, - ColorProperty, - EnumProperty, - FractionProperty, - Inspectable, - NameProperty, - NumberProperty, - PathProperty, - Property, -} from 'app/pages/editor/model/properties'; -import { MathUtil, Matrix, Rect } from 'app/pages/editor/scripts/common'; -import * as _ from 'lodash'; - -type Type = 'vector' | 'group' | 'mask' | 'path'; - -/** - * Interface that is shared by all vector drawable layer models below. - */ -@Property.register(new NameProperty('name')) -export abstract class Layer implements Inspectable, Animatable { - /** - * A non-user-visible string that uniquely identifies this layer in the tree. - */ - id?: string; - - /** - * A user-visible string uniquely identifying this layer in the tree. This value - * can be renamed, as long as it doesn't conflict with other layers in the tree. - */ - name: string; - - /** - * This layers children list of layers. - */ - children: ReadonlyArray; - - /** - * Returns the Layer type. This string value should not change, - * as it is used to identify the layer type and icon. - */ - abstract type: Type; - - /** - * Returns the bounding box for this Layer (or undefined if none exists). - */ - abstract bounds: Rect | undefined; - - constructor(obj: LayerConstructorArgs) { - this.id = obj.id || _.uniqueId(); - this.name = obj.name || ''; - this.children = (obj.children || []).map(child => load(child)); - } - - /** - * Returns the first descendent layer with the specified id. - */ - findLayerById(id: string): Layer | undefined { - if (this.id === id) { - return this; - } - for (const child of this.children) { - const layer = child.findLayerById(id); - if (layer) { - return layer; - } - } - return undefined; - } - - /** - * Returns the first descendent layer with the specified name. - */ - findLayerByName(name: string): Layer | undefined { - if (this.name === name) { - return this; - } - for (const child of this.children) { - const layer = child.findLayerByName(name); - if (layer) { - return layer; - } - } - return undefined; - } - - /** - * Walks the layer tree, executing beforeFunc on each node using a - * preorder traversal. - */ - walk(beforeFn: (layer: Layer) => void) { - const visitFn = (layer: Layer) => { - beforeFn(layer); - layer.children.forEach(l => visitFn(l)); - }; - visitFn(this); - } - - /** - * Returns the JSON representation of this layer. - */ - toJSON() { - return { - id: this.id, - name: this.name, - type: this.type, - }; - } - - /** - * Returns a shallow clone of this Layer. - */ - abstract clone(): Layer; - - /** - * Returns a deep clone of this Layer. - */ - abstract deepClone(): Layer; -} - -// TODO: share this interface with Layer? -interface LayerArgs { - id?: string; - name: string; - children: ReadonlyArray; -} - -export interface Layer extends LayerArgs, Inspectable, Animatable {} -export interface LayerConstructorArgs extends LayerArgs {} - -function load(obj: Layer | any): Layer { - if (obj instanceof Layer) { - return obj; - } - if (obj.type === 'vector') { - return new VectorLayer(obj); - } - if (obj.type === 'group') { - return new GroupLayer(obj); - } - if (obj.type === 'path') { - return new PathLayer(obj); - } - if (obj.type === 'mask') { - return new ClipPathLayer(obj); - } - console.error('Attempt to load layer with invalid object: ', obj); - throw new Error('Attempt to load layer with invalid object'); -} - -const VECTOR_DEFAULTS = { - canvasColor: '', - alpha: 1, -}; - -/** - * Model object that mirrors the VectorDrawable's '' element. - */ -@Property.register( - new ColorProperty('canvasColor'), - new NumberProperty('width', { isAnimatable: false, min: 1, isInteger: true }), - new NumberProperty('height', { isAnimatable: false, min: 1, isInteger: true }), - new FractionProperty('alpha', { isAnimatable: true }), -) -export class VectorLayer extends Layer { - // @Override - readonly type = 'vector'; - - constructor(obj = { children: [], name: 'vector' } as VectorConstructorArgs) { - super(obj); - const setterFn = (num: number, def: number) => (_.isNil(num) ? def : num); - this.canvasColor = obj.canvasColor || VECTOR_DEFAULTS.canvasColor; - this.width = setterFn(obj.width, 24); - this.height = setterFn(obj.height, 24); - this.alpha = setterFn(obj.alpha, VECTOR_DEFAULTS.alpha); - } - - // @Override - get bounds() { - return { l: 0, t: 0, r: this.width, b: this.height }; - } - - // @Override - clone() { - const clone = new VectorLayer(this); - clone.children = [...this.children]; - return clone; - } - - // @Override - deepClone() { - const clone = this.clone(); - clone.children = this.children.map(c => c.deepClone()); - return clone; - } - - // @Override - toJSON() { - const obj = Object.assign(super.toJSON(), { - canvasColor: this.canvasColor, - width: this.width, - height: this.height, - alpha: this.alpha, - children: this.children.map(child => child.toJSON()), - }); - Object.entries(VECTOR_DEFAULTS).forEach(([key, value]) => { - if ((obj as any)[key] === value) { - delete (obj as any)[key]; - } - }); - return obj; - } -} - -interface VectorLayerArgs { - canvasColor?: string; - width?: number; - height?: number; - alpha?: number; -} - -export interface VectorLayer extends Layer, VectorLayerArgs {} -export interface VectorConstructorArgs extends LayerConstructorArgs, VectorLayerArgs {} - -const GROUP_DEFAULTS = { - rotation: 0, - scaleX: 1, - scaleY: 1, - pivotX: 0, - pivotY: 0, - translateX: 0, - translateY: 0, -}; - -/** - * Model object that mirrors the VectorDrawable's '' element. - */ -@Property.register( - new NumberProperty('rotation', { isAnimatable: true }), - new NumberProperty('scaleX', { isAnimatable: true }), - new NumberProperty('scaleY', { isAnimatable: true }), - new NumberProperty('pivotX', { isAnimatable: true }), - new NumberProperty('pivotY', { isAnimatable: true }), - new NumberProperty('translateX', { isAnimatable: true }), - new NumberProperty('translateY', { isAnimatable: true }), -) -export class GroupLayer extends Layer { - // @Override - readonly type = 'group'; - - constructor(obj: GroupConstructorArgs) { - super(obj); - const setterFn = (num: number, def: number) => (_.isNil(num) ? def : num); - this.pivotX = setterFn(obj.pivotX, GROUP_DEFAULTS.pivotX); - this.pivotY = setterFn(obj.pivotY, GROUP_DEFAULTS.pivotY); - this.rotation = setterFn(obj.rotation, GROUP_DEFAULTS.rotation); - this.scaleX = setterFn(obj.scaleX, GROUP_DEFAULTS.scaleX); - this.scaleY = setterFn(obj.scaleY, GROUP_DEFAULTS.scaleY); - this.translateX = setterFn(obj.translateX, GROUP_DEFAULTS.translateX); - this.translateY = setterFn(obj.translateY, GROUP_DEFAULTS.translateY); - } - - // @Override - get bounds() { - let bounds: { l: number; t: number; r: number; b: number }; - this.children.forEach(child => { - const childBounds = child.bounds; - if (!childBounds) { - return; - } - if (bounds) { - bounds.l = Math.min(childBounds.l, bounds.l); - bounds.t = Math.min(childBounds.t, bounds.t); - bounds.r = Math.max(childBounds.r, bounds.r); - bounds.b = Math.max(childBounds.b, bounds.b); - } else { - bounds = { ...childBounds }; - } - }); - if (!bounds) { - return undefined; - } - bounds.l -= this.pivotX; - bounds.t -= this.pivotY; - bounds.r -= this.pivotX; - bounds.b -= this.pivotY; - const transforms = [ - Matrix.scaling(this.scaleX, this.scaleY), - Matrix.rotation(this.rotation), - Matrix.translation(this.translateX, this.translateY), - ]; - const topLeft = MathUtil.transformPoint({ x: bounds.l, y: bounds.t }, ...transforms); - const bottomRight = MathUtil.transformPoint({ x: bounds.r, y: bounds.b }, ...transforms); - return { - l: topLeft.x + this.pivotX, - t: topLeft.y + this.pivotY, - r: bottomRight.x + this.pivotX, - b: bottomRight.y + this.pivotY, - }; - } - - // @Override - clone() { - const clone = new GroupLayer(this); - clone.children = [...this.children]; - return clone; - } - - // @Override - deepClone() { - const clone = this.clone(); - clone.children = this.children.map(c => c.deepClone()); - return clone; - } - - // @Override - toJSON() { - const obj = Object.assign(super.toJSON(), { - rotation: this.rotation, - scaleX: this.scaleX, - scaleY: this.scaleY, - pivotX: this.pivotX, - pivotY: this.pivotY, - translateX: this.translateX, - translateY: this.translateY, - children: this.children.map(child => child.toJSON()), - }); - Object.entries(GROUP_DEFAULTS).forEach(([key, value]) => { - if ((obj as any)[key] === value) { - delete (obj as any)[key]; - } - }); - return obj; - } -} - -interface GroupLayerArgs { - pivotX?: number; - pivotY?: number; - rotation?: number; - scaleX?: number; - scaleY?: number; - translateX?: number; - translateY?: number; -} - -export interface GroupLayer extends Layer, GroupLayerArgs {} -export interface GroupConstructorArgs extends LayerConstructorArgs, GroupLayerArgs {} - -/** - * Model object that mirrors the VectorDrawable's '' element. - */ -@Property.register(new PathProperty('pathData', { isAnimatable: true })) -export class ClipPathLayer extends Layer implements MorphableLayer { - // @Override - readonly type = 'mask'; - - constructor(obj: ClipPathConstructorArgs) { - super(obj); - this.pathData = obj.pathData; - } - - // @Override - get bounds() { - return this.pathData ? this.pathData.getBoundingBox() : undefined; - } - - // @Override - clone() { - return new ClipPathLayer(this); - } - - // @Override - deepClone() { - return this.clone(); - } - - // @Override - toJSON() { - return Object.assign(super.toJSON(), { - pathData: this.pathData ? this.pathData.getPathString() : '', - }); - } - - isStroked() { - // TODO: this may be the case for Android... but does this limit what web/iOS devs can do? - return false; - } - - isFilled() { - return true; - } -} - -interface ClipPathLayerArgs { - pathData: Path; -} - -export interface ClipPathLayer extends Layer, ClipPathLayerArgs {} -export interface ClipPathConstructorArgs extends LayerConstructorArgs, ClipPathLayerArgs {} - -const ENUM_LINECAP_OPTIONS = [ - { value: 'butt', label: 'Butt' }, - { value: 'square', label: 'Square' }, - { value: 'round', label: 'Round' }, -]; - -const ENUM_LINEJOIN_OPTIONS = [ - { value: 'miter', label: 'Miter' }, - { value: 'round', label: 'Round' }, - { value: 'bevel', label: 'Bevel' }, -]; - -const ENUM_FILLTYPE_OPTIONS = [ - { value: 'nonZero', label: 'nonZero' }, - { value: 'evenOdd', label: 'evenOdd' }, -]; - -const PATH_DEFAULTS = { - fillColor: '', - fillAlpha: 1, - strokeColor: '', - strokeAlpha: 1, - strokeWidth: 0, - strokeLinecap: 'butt' as StrokeLineCap, - strokeLinejoin: 'miter' as StrokeLineJoin, - strokeMiterLimit: 4, - trimPathStart: 0, - trimPathEnd: 1, - trimPathOffset: 0, - fillType: 'nonZero' as FillType, -}; - -/** - * Model object that mirrors the VectorDrawable's '' element. - */ -@Property.register( - new PathProperty('pathData', { isAnimatable: true }), - new ColorProperty('fillColor', { isAnimatable: true }), - new FractionProperty('fillAlpha', { isAnimatable: true }), - new ColorProperty('strokeColor', { isAnimatable: true }), - new FractionProperty('strokeAlpha', { isAnimatable: true }), - new NumberProperty('strokeWidth', { min: 0, isAnimatable: true }), - new EnumProperty('strokeLinecap', ENUM_LINECAP_OPTIONS), - new EnumProperty('strokeLinejoin', ENUM_LINEJOIN_OPTIONS), - new NumberProperty('strokeMiterLimit', { min: 1 }), - new FractionProperty('trimPathStart', { isAnimatable: true }), - new FractionProperty('trimPathEnd', { isAnimatable: true }), - new FractionProperty('trimPathOffset', { isAnimatable: true }), - new EnumProperty('fillType', ENUM_FILLTYPE_OPTIONS), -) // TODO: need to fix enum properties so they store/return strings instead of options? -export class PathLayer extends Layer implements MorphableLayer { - // @Override - readonly type = 'path'; - - constructor(obj: PathConstructorArgs) { - super(obj); - const setterFn = (num: number, def: number) => (_.isNil(num) ? def : num); - this.pathData = obj.pathData; - this.fillColor = obj.fillColor || PATH_DEFAULTS.fillColor; - this.fillAlpha = setterFn(obj.fillAlpha, PATH_DEFAULTS.fillAlpha); - this.strokeColor = obj.strokeColor || PATH_DEFAULTS.strokeColor; - this.strokeAlpha = setterFn(obj.strokeAlpha, PATH_DEFAULTS.strokeAlpha); - this.strokeWidth = setterFn(obj.strokeWidth, PATH_DEFAULTS.strokeWidth); - this.strokeLinecap = obj.strokeLinecap || PATH_DEFAULTS.strokeLinecap; - this.strokeLinejoin = obj.strokeLinejoin || PATH_DEFAULTS.strokeLinejoin; - this.strokeMiterLimit = setterFn(obj.strokeMiterLimit, PATH_DEFAULTS.strokeMiterLimit); - this.trimPathStart = setterFn(obj.trimPathStart, PATH_DEFAULTS.trimPathStart); - this.trimPathEnd = setterFn(obj.trimPathEnd, PATH_DEFAULTS.trimPathEnd); - this.trimPathOffset = setterFn(obj.trimPathOffset, PATH_DEFAULTS.trimPathOffset); - this.fillType = obj.fillType || PATH_DEFAULTS.fillType; - } - - // @Override - get bounds() { - return this.pathData ? this.pathData.getBoundingBox() : undefined; - } - - // @Override - clone() { - return new PathLayer(this); - } - - // @Override - deepClone() { - return this.clone(); - } - - // @Override - toJSON() { - const obj = Object.assign(super.toJSON(), { - pathData: this.pathData ? this.pathData.getPathString() : '', - fillColor: this.fillColor, - fillAlpha: this.fillAlpha, - strokeColor: this.strokeColor, - strokeAlpha: this.strokeAlpha, - strokeWidth: this.strokeWidth, - strokeLinecap: this.strokeLinecap, - strokeLinejoin: this.strokeLinejoin, - strokeMiterLimit: this.strokeMiterLimit, - trimPathStart: this.trimPathStart, - trimPathEnd: this.trimPathEnd, - trimPathOffset: this.trimPathOffset, - fillType: this.fillType, - }); - Object.entries(PATH_DEFAULTS).forEach(([key, value]) => { - if ((obj as any)[key] === value) { - delete (obj as any)[key]; - } - }); - return obj; - } - - isStroked() { - return !!this.strokeColor; - } - - isFilled() { - return !!this.fillColor; - } -} - -interface PathLayerArgs { - pathData: Path; - fillColor?: string; - fillAlpha?: number; - strokeColor?: string; - strokeAlpha?: number; - strokeWidth?: number; - strokeLinecap?: StrokeLineCap; - strokeLinejoin?: StrokeLineJoin; - strokeMiterLimit?: number; - trimPathStart?: number; - trimPathEnd?: number; - trimPathOffset?: number; - fillType?: FillType; -} - -export interface PathLayer extends Layer, PathLayerArgs {} -export interface PathConstructorArgs extends LayerConstructorArgs, PathLayerArgs {} - -export type StrokeLineCap = 'butt' | 'square' | 'round'; -export type StrokeLineJoin = 'miter' | 'round' | 'bevel'; -export type FillType = 'nonZero' | 'evenOdd'; - -/** Common interface for Layers with pathData properties. */ -export interface MorphableLayer extends Layer { - pathData: Path; - isStroked(): boolean; - isFilled(): boolean; -} diff --git a/src/app/pages/editor/model/layers/LayerUtil.ts b/src/app/pages/editor/model/layers/LayerUtil.ts deleted file mode 100644 index c71cdce1..00000000 --- a/src/app/pages/editor/model/layers/LayerUtil.ts +++ /dev/null @@ -1,343 +0,0 @@ -import { Path } from 'app/pages/editor/model/paths'; -import { MathUtil, Matrix } from 'app/pages/editor/scripts/common'; -import { environment } from 'environments/environment'; -import * as _ from 'lodash'; - -import { ClipPathLayer, GroupLayer, Layer, PathLayer, VectorLayer } from './Layer'; - -const IS_DEV_BUILD = !environment.production; - -/** - * Returns a single flattened transform matrix that can be used to perform canvas - * transform operations. The resulting matrix will transform path coordinates to - * canvas drawing coordinates. The inverse of the matrix will transform canvas - * drawing coordinates back to path coordinates. - */ -export function getCanvasTransformForLayer(root: Layer, layerId: string) { - return Matrix.flatten(getCanvasTransformsForLayer(root, layerId)); -} - -/** - * Returns a list of parent transforms for the specified layer ID. The transforms - * are returned in top-down order (i.e. the transform for the layer's - * immediate parent will be the very last matrix in the returned list). - */ -function getCanvasTransformsForLayer(root: Layer, layerId: string) { - return (function recurseFn(parents: Layer[], current: Layer): Matrix[] { - if (current.id === layerId) { - return _.flatMap(parents, l => { - return l instanceof GroupLayer ? getCanvasTransformsForGroupLayer(l) : []; - }); - } - for (const child of current.children) { - const transforms = recurseFn([...parents, current], child); - if (transforms) { - return transforms; - } - } - return undefined; - })([], root); -} - -/** - * Returns a list of matrix transforms for a given group layer. - */ -export function getCanvasTransformsForGroupLayer(l: GroupLayer) { - // First negative pivot, then scale, then rotation, then translation, then pivot. - // When drawing a path, the transforms are applied at the bottom up, which - // is why the order appears to be reversed below. - return [ - Matrix.translation(l.pivotX, l.pivotY), - Matrix.translation(l.translateX, l.translateY), - Matrix.rotation(l.rotation), - Matrix.scaling(l.scaleX, l.scaleY), - Matrix.translation(-l.pivotX, -l.pivotY), - ]; -} - -/** - * Makes two vector layers with possibly different viewports compatible with each other. - */ -export function adjustViewports(vl1: VectorLayer, vl2: VectorLayer) { - if (!vl1 || !vl2) { - return { vl1, vl2 }; - } - - vl1 = vl1.deepClone(); - vl2 = vl2.deepClone(); - - let { width: w1, height: h1 } = vl1; - let { width: w2, height: h2 } = vl2; - const isMaxDimenFn = (n: number) => Math.max(w1, h1, w2, h2, n) === n; - - let scale1 = 1; - let scale2 = 1; - if (isMaxDimenFn(w1)) { - scale2 = w1 / w2; - } else if (isMaxDimenFn(h1)) { - scale2 = h1 / h2; - } else if (isMaxDimenFn(w2)) { - scale1 = w2 / w1; - } else { - scale1 = h2 / h1; - } - - if (isMaxDimenFn(w1) || isMaxDimenFn(h1)) { - w1 = MathUtil.round(w1); - h1 = MathUtil.round(h1); - w2 = MathUtil.round(w2 * scale2); - h2 = MathUtil.round(h2 * scale2); - } else { - w1 = MathUtil.round(w1 * scale1); - h1 = MathUtil.round(h1 * scale1); - w2 = MathUtil.round(w2); - h2 = MathUtil.round(h2); - } - - let tx1 = 0; - let ty1 = 0; - let tx2 = 0; - let ty2 = 0; - if (w1 > w2) { - tx2 = (w1 - w2) / 2; - } else if (w1 < w2) { - tx1 = (w2 - w1) / 2; - } else if (h1 > h2) { - ty2 = (h1 - h2) / 2; - } else if (h1 < h2) { - ty1 = (h2 - h1) / 2; - } - - const transformLayerFn = (vl: VectorLayer, scale: number, tx: number, ty: number) => { - const transforms = Matrix.flatten([Matrix.scaling(scale, scale), Matrix.translation(tx, ty)]); - (function recurseFn(layer: Layer) { - if (layer instanceof PathLayer || layer instanceof ClipPathLayer) { - if (layer instanceof PathLayer && layer.isStroked()) { - layer.strokeWidth *= scale; - } - if (layer.pathData) { - layer.pathData = new Path( - layer.pathData.getCommands().map(cmd => - cmd - .mutate() - .transform(transforms) - .build(), - ), - ); - } - return; - } - if (layer instanceof GroupLayer) { - const l = layer as GroupLayer; - l.translateX *= scale; - l.translateY *= scale; - l.pivotX *= scale; - l.pivotY *= scale; - } - layer.children.forEach(l => recurseFn(l)); - })(vl); - }; - - transformLayerFn(vl1, scale1, tx1, ty1); - transformLayerFn(vl2, scale2, tx2, ty2); - - const newWidth = Math.max(w1, w2); - const newHeight = Math.max(h1, h2); - vl1.width = newWidth; - vl2.width = newWidth; - vl1.height = newHeight; - vl2.height = newHeight; - return { vl1, vl2 }; -} - -export function mergeVectorLayers(vl1: VectorLayer, vl2: VectorLayer) { - const { vl1: newVl1, vl2: newVl2 } = adjustViewports(vl1, vl2); - const vl = setLayerChildren(newVl1, [...newVl1.children, ...newVl2.children]); - if (!newVl1.children.length) { - // Only replace the vector layer's alpha if there are no children - // being displayed to the user. This is pretty much the best - // we can do. - vl.alpha = newVl2.alpha; - } - return vl; -} - -/** - * Adds a list of children to a parent layer in a vector layer tree. - * @param root the root vector layer - * @param addedLayerParentId the parent layer in which to add the given layers - * @param startingChildIndex the index to start adding the layers - * @param addedLayers the layers to add - */ -export function addLayers( - root: VectorLayer, - addedLayerParentId: string, - startingChildIndex: number, - ...addedLayers: Layer[] -) { - return (function recurseFn(curr: Layer) { - if (curr.id === addedLayerParentId) { - // If we have reached the added layer's parent, then - // clone the parent, insert the new layer into its list - // of children, and return the new parent node. - const children = [...curr.children]; - children.splice(startingChildIndex, 0, ...addedLayers); - return setLayerChildren(curr, children); - } - for (let i = 0; i < curr.children.length; i++) { - const clonedChild = recurseFn(curr.children[i]); - if (clonedChild) { - // Then clone the current layer, insert the cloned child - // into its list of children, and return the cloned current layer. - const children = [...curr.children]; - children[i] = clonedChild; - return setLayerChildren(curr, children); - } - } - return undefined; - })(root) as VectorLayer; -} - -export function removeLayers(layer: L, ...removedLayerIds: string[]) { - const layerIds = new Set(removedLayerIds); - return (function recurseFn(curr: Layer): Layer { - if (layerIds.has(curr.id)) { - return undefined; - } - const children = curr.children.map(recurseFn).filter(l => !!l); - return setLayerChildren(curr, children); - })(layer) as L; -} - -export function updateLayer(vl: VectorLayer, layer: Layer) { - return replaceLayer(vl, layer.id, layer); -} - -export function replaceLayer(vl: VectorLayer, layerId: string, replacement: Layer) { - if (IS_DEV_BUILD && !vl.findLayerById(layerId)) { - console.warn('Attempt to replace a layer that does not exist in the tree'); - } - return (function recurseFn(curr: Layer): Layer { - return curr.id === layerId - ? replacement - : setLayerChildren(curr, curr.children.map(child => recurseFn(child))); - })(vl) as VectorLayer; -} - -export function runPreorderTraversal(layer: Layer) { - // Add the layers as we iterate the tree to ensure they are properly sorted. - const layers: Layer[] = []; - (function recurseFn(l: Layer) { - layers.push(l); - l.children.forEach(recurseFn); - })(layer); - return layers; -} - -export function findLayerByName(layers: ReadonlyArray, layerName: string) { - for (const layer of layers) { - const target = layer.findLayerByName(layerName); - if (target) { - return target; - } - } - return undefined; -} - -export function findParent(vl: VectorLayer, layerId: string) { - return (function recurseFn(curr: Layer, parent?: Layer): Layer { - if (curr.id === layerId) { - return parent; - } - for (const child of curr.children) { - const p = recurseFn(child, curr); - if (p) { - return p; - } - } - return undefined; - })(vl); -} - -export function findNextSibling(vl: VectorLayer, layerId: string) { - return findSibling(layerId, findParent(vl, layerId), 1); -} - -export function findPreviousSibling(vl: VectorLayer, layerId: string) { - return findSibling(layerId, findParent(vl, layerId), -1); -} - -function findSibling(layerId: string, parent: Layer, offset: number) { - if (!parent || !parent.children) { - return undefined; - } - let index = _.findIndex(parent.children, c => c.id === layerId); - if (index < 0) { - return undefined; - } - index += offset; - if (index < 0 || parent.children.length <= index) { - return undefined; - } - return parent.children[index]; -} - -export function getUniqueLayerName(layers: ReadonlyArray, prefix: string) { - return getUniqueName(prefix, name => findLayerByName(layers, name)); -} - -export function getUniqueName(prefix = '', objectByNameFn = (s: string) => undefined as any) { - let n = 0; - const nameFn = () => prefix + (n ? `_${n}` : ''); - while (true) { - const o = objectByNameFn(nameFn()); - if (!o) { - break; - } - n++; - } - return nameFn(); -} - -/** - * Returns a cloned layer with the specified list of children layers. - */ -function setLayerChildren(layer: L, children: ReadonlyArray) { - const clone = layer.clone(); - clone.children = children; - return clone as L; -} - -export function toStrokeDashArray( - trimPathStart: number, - trimPathEnd: number, - trimPathOffset: number, - pathLength: number, - // TODO: remove this eventually... it is used to fix a canvas bug (that I am probably not handling correctly) - marginOfError = 0, -) { - // Calculate the visible fraction of the trimmed path. If trimPathStart - // is greater than trimPathEnd, then the result should be the combined - // length of the two line segments: [trimPathStart,1] and [0,trimPathEnd]. - let shownFraction = trimPathEnd - trimPathStart; - if (trimPathStart > trimPathEnd) { - shownFraction += 1; - } - // Calculate the dash array. The first array element is the length of - // the trimmed path and the second element is the gap, which is the - // difference in length between the total path length and the visible - // trimmed path length. - return [shownFraction * pathLength, (1 - shownFraction + marginOfError) * pathLength]; -} - -export function toStrokeDashOffset( - trimPathStart: number, - trimPathEnd: number, - trimPathOffset: number, - pathLength: number, -) { - // The amount to offset the path is equal to the trimPathStart plus - // trimPathOffset. We mod the result because the trimmed path - // should wrap around once it reaches 1. - return pathLength * (1 - ((trimPathStart + trimPathOffset) % 1)); -} diff --git a/src/app/pages/editor/model/layers/index.ts b/src/app/pages/editor/model/layers/index.ts deleted file mode 100644 index 4d08099b..00000000 --- a/src/app/pages/editor/model/layers/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as LayerUtil from './LayerUtil'; -export { LayerUtil }; - -export { - Layer, - ClipPathLayer, - VectorLayer, - GroupLayer, - PathLayer, - StrokeLineCap, - StrokeLineJoin, - FillType, - MorphableLayer, -} from './Layer'; diff --git a/src/app/pages/editor/model/layers/layers.spec.ts b/src/app/pages/editor/model/layers/layers.spec.ts deleted file mode 100644 index 19663281..00000000 --- a/src/app/pages/editor/model/layers/layers.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -// import { VectorLayer, ClipPathLayer, GroupLayer, PathLayer } from '.'; -// import { Path } from '../paths'; - -// describe('Layers', () => { -// it('ClipPathLayer', () => { -// const layer = new ClipPathLayer('myId', new Path('M 0 0 1 1 2 2 3 3')); -// console.info(layer); -// expect(layer.hasOwnProperty('id')).toBe(true); -// }); -// }); diff --git a/src/app/pages/editor/model/paper/CursorType.ts b/src/app/pages/editor/model/paper/CursorType.ts deleted file mode 100644 index da1b90bd..00000000 --- a/src/app/pages/editor/model/paper/CursorType.ts +++ /dev/null @@ -1,27 +0,0 @@ -// These names correspond to CSS class names declared in the root component. -export enum CursorType { - Default = 'default', - Pointer = 'pointer', - PointSelect = 'point-select', - Crosshair = 'crosshair', - Pen = 'pen', - PenAdd = 'pen-add', - PenClose = 'pen-close', - Pencil = 'pencil', - Resize0 = 'resize0', - Resize45 = 'resize45', - Resize90 = 'resize90', - Resize135 = 'resize135', - Rotate0 = 'rotate0', - Rotate45 = 'rotate45', - Rotate90 = 'rotate90', - Rotate135 = 'rotate135', - Rotate180 = 'rotate180', - Rotate225 = 'rotate225', - Rotate270 = 'rotate270', - Rotate315 = 'rotate315', - ZoomIn = 'zoom-in', - ZoomOut = 'zoom-out', - Grab = 'grab', - Grabbing = 'grabbing', -} diff --git a/src/app/pages/editor/model/paper/ToolMode.ts b/src/app/pages/editor/model/paper/ToolMode.ts deleted file mode 100644 index 9a871306..00000000 --- a/src/app/pages/editor/model/paper/ToolMode.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum ToolMode { - Default = 'Default', - Pencil = 'Pencil', - Ellipse = 'Ellipse', - Rectangle = 'Rectangle', - ZoomPan = 'ZoomPan', -} diff --git a/src/app/pages/editor/model/paper/index.ts b/src/app/pages/editor/model/paper/index.ts deleted file mode 100644 index 95550433..00000000 --- a/src/app/pages/editor/model/paper/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { ToolMode } from './ToolMode'; -export { CursorType } from './CursorType'; diff --git a/src/app/pages/editor/model/paths/Command.ts b/src/app/pages/editor/model/paths/Command.ts deleted file mode 100644 index a365f8de..00000000 --- a/src/app/pages/editor/model/paths/Command.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { MathUtil, Matrix, Point } from 'app/pages/editor/scripts/common'; -import * as _ from 'lodash'; - -import { SvgChar } from '.'; - -/** - * Represents an individual SVG command. These are the essential building blocks - * of all Paths and SubPath objects. - */ -export class Command { - constructor( - private readonly _type: SvgChar, - private readonly _points: ReadonlyArray, - private readonly _isSplitPoint = false, - private readonly _id = _.uniqueId(), - private readonly _isSplitSegment = false, - ) { - if (_type === undefined) { - throw new Error('Attempt to set an undefined svgChar'); - } - } - - /** - * Returns the unique ID for this command. - */ - get id() { - return this._id; - } - - /** - * Returns the SVG character for this command. - */ - get type() { - return this._type; - } - - /** - * Returns the points for this command. - */ - get points() { - return this._points; - } - - /** - * Returns true iff the command was created as a result of being split. - * Only split commands are able to be editted and deleted via the inspector/canvas. - */ - isSplitPoint() { - return this._isSplitPoint; - } - - /** - * Returns true iff the command was created as a result of a subpath split. - */ - isSplitSegment() { - return this._isSplitSegment; - } - - /** - * Returns the command's starting point. The starting point for the first - * command of the first subpath will be undefined. - */ - get start() { - return _.first(this._points); - } - - /** - * Returns the command's ending point. - */ - get end() { - return _.last(this._points); - } - - /** - * Returns true iff this command can be converted into a new command - * that is morphable with the specified SVG command type. - */ - canConvertTo(targetChar: SvgChar) { - const ch = targetChar; - if (this._type === 'M' || ch === 'M' || this._type === ch) { - return false; - } - switch (this._type) { - case 'L': - return ch === 'Q' || ch === 'C'; - case 'Z': - return ch === 'L' || ch === 'Q' || ch === 'C'; - case 'Q': { - const uniquePoints = _.uniqWith(this._points, MathUtil.arePointsEqual); - return ch === 'C' || (ch === 'L' && uniquePoints.length <= 2); - } - case 'C': { - const uniquePoints = _.uniqWith(this._points, MathUtil.arePointsEqual); - return ch === 'L' && uniquePoints.length <= 2; - } - } - return false; - } - - /** - * Returns a builder to construct a mutated Command. - */ - mutate() { - return new CommandBuilder( - this._type, - [...this._points], - this.isSplitPoint(), - this._id, - this._isSplitSegment, - ); - } - - toString() { - if (this._type === 'Z') { - return `${this._type}`; - } else { - const p = _.last(this._points); - const x = _.round(p.x, 3); - const y = _.round(p.y, 3); - return `${this._type} ${x}, ${y}`; - } - } -} - -export class CommandBuilder { - private matrix: Matrix = Matrix.identity(); - - constructor( - private svgChar: SvgChar, - private points: Point[], - private isSplitPoint = false, - private id = '', - private isSplitSegment = false, - ) {} - - setSvgChar(svgChar: SvgChar) { - this.svgChar = svgChar; - return this; - } - - setId(id: string) { - this.id = id; - return this; - } - - setPoints(...points: Point[]) { - this.points = points; - return this; - } - - toggleSplitPoint() { - return this.setIsSplitPoint(!this.isSplitPoint); - } - - setIsSplitPoint(isSplitPoint: boolean) { - this.isSplitPoint = isSplitPoint; - return this; - } - - setIsSplitSegment(isSplitSegment: boolean) { - this.isSplitSegment = isSplitSegment; - return this; - } - - transform(transform: Matrix) { - this.matrix = transform.dot(this.matrix); - return this; - } - - reverse() { - if (this.svgChar !== 'M' || this.points[0]) { - // The first move command of an SVG path has an undefined - // starting point, so no change is required in that case. - this.points.reverse(); - } - return this; - } - - build() { - return new Command( - this.svgChar, - this.points.map(p => (p ? MathUtil.transformPoint(p, this.matrix) : p)), - this.isSplitPoint, - this.id || _.uniqueId(), - this.isSplitSegment, - ); - } -} diff --git a/src/app/pages/editor/model/paths/CommandState.ts b/src/app/pages/editor/model/paths/CommandState.ts deleted file mode 100644 index 31851fb9..00000000 --- a/src/app/pages/editor/model/paths/CommandState.ts +++ /dev/null @@ -1,417 +0,0 @@ -import { MathUtil, Matrix, Point } from 'app/pages/editor/scripts/common'; -import * as _ from 'lodash'; - -import { Command, Projection, SvgChar } from '.'; -import { Calculator, Line, newCalculator } from './calculators'; - -/** - * Container class that encapsulates a Command's underlying state. - */ -export class CommandState { - constructor( - // The original un-mutated command. - private readonly backingCommand: Command, - // A command state object wraps around the initial SVG command and outputs - // a list of transformed commands resulting from splits, unsplits, - // conversions, etc. If the initial SVG command hasn't been modified, - // then a list containing the initial SVG command is returned. - private readonly commands: ReadonlyArray = [backingCommand], - // The list of mutations describes how the initial backing command - // has since been modified. Since the command state always holds a - // reference to its initial backing command, these modifications - // can be reversed simply by removing mutations from the list. - private readonly mutations: ReadonlyArray = [ - { - id: backingCommand.id, - t: 1, - svgChar: backingCommand.type, - }, - ], - // The transformation matricies used to transform this command state object. - private readonly transform: Matrix = Matrix.identity(), - // The calculator that will do all of the math-y stuff for us. - private readonly calculator: Calculator = newCalculator(backingCommand), - // The lower bound T value (may be > 0 for split subpaths). - private readonly minT = 0, - // The upper bound T value (may be < 1 for split subpaths). - private readonly maxT = 1, - // When a filled subpath is split, we assign a 'split command id' to the two - // lines that are created (so that during unsplit operations we can identify - // which segments were added together). - private readonly splitSegmentId = '', - // The parent command state object (i.e. the one that created the new split segment). - private readonly parentCommandState?: CommandState, - ) {} - - getBackingId() { - return this.backingCommand.id; - } - - getCommands() { - return this.commands; - } - - getBoundingBox() { - return this.calculator.getBoundingBox(); - } - - intersects(line: Line) { - return this.calculator.intersects(line).filter(t => this.minT < t && t <= this.maxT); - } - - getIdAtIndex(splitIdx: number) { - return this.mutations[splitIdx].id; - } - - getPathLength() { - return this.calculator.getPathLength(); - } - - getPointAtLength(distance: number) { - return this.calculator.getPointAtLength(distance); - } - - project(point: Point): { projection: Projection; splitIdx: number } | undefined { - const projection = this.calculator.project(point); - if (!projection) { - return undefined; - } - const projT = projection.t; - if (projT < this.minT || this.maxT < projT) { - // If this happens, then the projection is being mapped to some other - // split command segment. - // TODO: recompute the projection so that it properly returned the correct value... - // console.warn('Failed to compute projection for CommandState'); - return undefined; - } - // Count the number of t values that are less than the projection. - const splitIdx = _.sumBy(this.mutations, m => (m.t < projection.t ? 1 : 0)); - const tempSplits = [this.minT, ...this.mutations.map(m => m.t)]; - const startSplit = tempSplits[splitIdx]; - const endSplit = tempSplits[splitIdx + 1]; - // Update the t value so that it is in relation to the client-visible subIdx and cmdIdx. - projection.t = - startSplit === endSplit ? 0 : (projection.t - startSplit) / (endSplit - startSplit); - return { projection, splitIdx }; - } - - /** - * Slices the command state object into two parts. Useful for subpath splitting. - */ - slice(splitIdx: number) { - const left = this.mutate() - .sliceLeft(splitIdx) - .build(); - let right: CommandState; - if (this.isSplitAtIndex(splitIdx)) { - right = this.mutate() - .sliceRight(splitIdx) - .build(); - } - return { left, right }; - } - - /** - * Merges two previously sliced command state objects into one. - */ - merge(cs: CommandState) { - if (this.getBackingId() !== cs.getBackingId()) { - throw new Error('Attempt to merge command state objects with unequal backing IDs'); - } - if (this.minT < cs.minT) { - console.warn('Merging command states out of order', this, cs); - } - return this.mutate() - .setMutations([...cs.mutations.slice(0, cs.mutations.length - 1), ...this.mutations]) - .setMinT(cs.minT) - .build(); - } - - /** - * Returns true iff the command at the specified index is split. - */ - isSplitAtIndex(splitIdx: number) { - return splitIdx !== this.mutations.length - 1; - } - - getSplitSegmentId() { - return this.splitSegmentId; - } - - getParentCommandState() { - return this.parentCommandState; - } - - mutate() { - return new CommandStateMutator( - this.backingCommand, - [...this.mutations], - this.transform, - this.calculator, - this.minT, - this.maxT, - this.splitSegmentId, - this.parentCommandState, - ); - } -} - -interface Mutation { - readonly id: string; - readonly t: number; - readonly svgChar: SvgChar; -} - -/** - * A builder class for creating new mutated CommandState objects. - */ -class CommandStateMutator { - constructor( - private backingCommand: Command, - private mutations: Mutation[], - private matrix: Matrix, - private calculator: Calculator, - private minT: number, - private maxT: number, - private splitSegmentId: string, - private parentCommandState: CommandState, - ) {} - - /** - * Slices this command state object at the specified index, discarding - * anything to the right. - */ - sliceLeft(splitIdx: number) { - this.mutations = this.mutations.slice(0, splitIdx + 1).map(m => _.clone(m)); - this.maxT = _.last(this.mutations).t; - return this; - } - - /** - * Slices this command state object at the specified index, discarding - * anything to the left. - */ - sliceRight(splitIdx: number) { - this.minT = this.mutations[splitIdx].t; - this.mutations = this.mutations.slice(splitIdx + 1).map(m => _.clone(m)); - return this; - } - - setMutations(mutations: ReadonlyArray) { - this.mutations = [...mutations]; - return this; - } - - setMinT(minT: number) { - this.minT = minT; - return this; - } - - /** - * Sets this command state object as a split segment with a unique ID. - * The parent state object represents the origin command state. - */ - setSplitSegmentInfo(parentCommandState: CommandState, id: string) { - this.splitSegmentId = id; - this.parentCommandState = parentCommandState; - return this; - } - - /** - * Reverses the information stored by this command state object. - */ - reverse() { - this.backingCommand = this.backingCommand - .mutate() - .reverse() - .build(); - this.calculator = newCalculator(this.backingCommand); - const lastMutation = this.mutations.pop(); - this.mutations = this.mutations - .map(m => { - const { id, svgChar } = m; - return { id, svgChar, t: MathUtil.lerp(this.maxT, this.minT, m.t) }; - }) - .reverse(); - this.mutations.push(lastMutation); - return this; - } - - /** - * Inserts the provided t values at the specified split index. The t values - * are linearly interpolated between the split values at splitIdx and - * splitIdx + 1 to ensure the split is done in relation to the mutated command. - */ - splitAtIndex(splitIdx: number, ts: ReadonlyArray) { - const tempSplits = [this.minT, ...this.mutations.map(m => m.t)]; - const startSplit = tempSplits[splitIdx]; - const endSplit = tempSplits[splitIdx + 1]; - return this.split(ts.map(t => MathUtil.lerp(startSplit, endSplit, t))); - } - - /** - * Same as splitAtIndex() except the command is split into two approximately - * equal parts. - */ - splitInHalfAtIndex(splitIdx: number) { - const tempSplits = [this.minT, ...this.mutations.map(m => m.t)]; - const startSplit = tempSplits[splitIdx]; - const endSplit = tempSplits[splitIdx + 1]; - const distance = MathUtil.lerp(startSplit, endSplit, 0.5); - return this.split([this.calculator.findTimeByDistance(distance)]); - } - - private split(ts: ReadonlyArray) { - if (!ts.length || this.backingCommand.type === 'M') { - return this; - } - const currSplits = this.mutations.map(m => m.t); - const currSvgChars = this.mutations.map(m => m.svgChar); - for (const t of ts) { - const id = _.uniqueId(); - const svgChar = currSvgChars[_.sortedIndex(currSplits, t)]; - const mutation = { id, t, svgChar }; - const insertionIdx = _.sortedIndexBy(this.mutations, mutation, m => m.t); - this.mutations.splice(insertionIdx, 0, { id, t, svgChar }); - } - for (let i = 0; i < this.mutations.length - 1; i++) { - const mutation = this.mutations[i]; - if (mutation.svgChar === 'Z') { - // Force convert the split closepath command into a line. - const { id, t } = mutation; - this.mutations[i] = { id, t, svgChar: 'L' }; - } - } - return this; - } - - /** - * Unsplits the command at the specified split index. - */ - unsplitAtIndex(splitIdx: number) { - if (!this.isSplitAtIndex(splitIdx)) { - console.warn('Ignoring attempt to unsplit a non-split command', this); - return this; - } - this.mutations.splice(splitIdx, 1); - return this; - } - - /** - * Returns true iff the command at the specified index is split. - */ - private isSplitAtIndex(splitIdx: number) { - return splitIdx !== this.mutations.length - 1; - } - - /** - * Converts the command at the specified split index. - */ - convertAtIndex(splitIdx: number, svgChar: SvgChar) { - const { id, t } = this.mutations[splitIdx]; - this.mutations[splitIdx] = { id, t, svgChar }; - return this; - } - - /** - * Unconverts all conversions previously performed on this - * command state object. - */ - unconvertSubpath() { - const backingSvgChar = this.backingCommand.type; - this.mutations = this.mutations.map((mutation, i) => { - let svgChar = backingSvgChar; - if (backingSvgChar === 'Z' && i !== this.mutations.length - 1) { - // Force convert the split closepath command back into a line. - svgChar = 'L'; - } - const { id, t } = mutation; - return { id, t, svgChar }; - }); - return this; - } - - /** - * Converts closepath commands to lines. This method irreversibly builds - * a new backing command to use. - */ - forceConvertClosepathsToLines() { - if (this.backingCommand.type === 'Z') { - this.backingCommand = this.calculator.convert('L').toCommand(); - this.calculator = newCalculator(this.backingCommand); - this.mutations = this.mutations.map(m => { - const { id, t, svgChar } = m; - const newSvgChar = svgChar === 'Z' ? 'L' : svgChar; - return { id, t, svgChar: newSvgChar }; - }); - } - return this; - } - - /** - * Adds transforms to this command state object using the - * specified transformation matrices. - */ - transform(transform: Matrix) { - this.matrix = transform.dot(this.matrix); - this.calculator = newCalculator( - this.backingCommand - .mutate() - .transform(this.matrix) - .build(), - ); - return this; - } - - /** - * Reverts this command state object back to its original state. - */ - revert() { - this.mutations = [ - { - id: _.last(this.mutations).id, - t: _.last(this.mutations).t, - svgChar: this.backingCommand.type, - }, - ]; - this.matrix = Matrix.identity(); - this.calculator = newCalculator(this.backingCommand); - return this; - } - - /** - * Builds a new command state object. - */ - build() { - // TODO: this could be more efficient (avoid recreating commands unnecessarily) - const builtCommands: Command[] = []; - let prevT = this.minT; - for (let i = 0; i < this.mutations.length; i++) { - const currT = this.mutations[i].t; - const isSplitSegment = this.mutations[i].svgChar !== 'M' && !!this.parentCommandState; - builtCommands.push( - this.calculator - .split(prevT, currT) - .convert(this.mutations[i].svgChar) - .toCommand() - .mutate() - .setId(this.mutations[i].id) - .setIsSplitPoint(i !== this.mutations.length - 1) - .setIsSplitSegment(isSplitSegment) - .build(), - ); - prevT = currT; - } - return new CommandState( - this.backingCommand, - builtCommands, - this.mutations, - this.matrix, - this.calculator, - this.minT, - this.maxT, - this.splitSegmentId, - this.parentCommandState, - ); - } -} diff --git a/src/app/pages/editor/model/paths/Path.spec.ts b/src/app/pages/editor/model/paths/Path.spec.ts deleted file mode 100644 index 13e8c4d6..00000000 --- a/src/app/pages/editor/model/paths/Path.spec.ts +++ /dev/null @@ -1,990 +0,0 @@ -import { MathUtil, Point } from 'app/pages/editor/scripts/common'; -import * as _ from 'lodash'; -import * as PathUtil from 'test/PathUtil'; - -import { Command } from './Command'; -import { Path, ProjectionOntoPath } from './Path'; -import { SvgChar } from './SvgChar'; - -const lerp = MathUtil.lerp; -const fromPathOpString = PathUtil.fromPathOpString; - -describe('Path', () => { - describe('constructing new Path objects', () => { - function buildPath(svgChars: string) { - const numSvgCharArgsFn = (svgChar: SvgChar) => { - switch (svgChar) { - case 'M': - case 'L': - return 2; - case 'Q': - return 4; - case 'C': - return 6; - case 'Z': - return 0; - } - }; - return new Path( - svgChars - .split('') - .map((svgChar: SvgChar) => { - const args = '5' - .repeat(numSvgCharArgsFn(svgChar)) - .split('') - .join(' '); - return svgChar === 'Z' ? 'Z' : `${svgChar} ${args}`; - }) - .join(' '), - ); - } - - const TESTS = [ - { - desc: 'construct a Path containing one subpath', - actual: 'MLLZ', - expected: ['MLLZ', 1], - }, - { - desc: 'construct a Path containing two subpaths', - actual: 'MLCQLZMZ', - expected: ['MLCQLZMZ', 2], - }, - // TODO: fix this test (SVGO probably makes it impossible, but just in case...) - // { - // desc: 'construct a Path w/ multiple moveto commands', - // actual: 'MMMMMLLLLL', - // expected: ['MMMMMLLLLL', 5], - // }, - { - desc: 'construct a Path w/ multiple closepath commands', - actual: 'MLCQLZMZZZZMLZMZZ', - expected: ['MLCQLZMZMZMZMZMLZMZMZ', 8], - }, - { - desc: 'construct a complex compound path with multiple subpaths', - actual: 'MLZMLLZMLMLLZLZLLZMLLMZM', - expected: ['MLZMLLZMLMLLZMLZMLLZMLLMZM', 9], - }, - ]; - - for (const test of TESTS) { - it(test.desc, () => { - const actualPath = buildPath(test.actual); - const actualSvgChars = _.flatMap(actualPath.getSubPaths(), subPath => { - return subPath.getCommands().map(cmd => cmd.type); - }).join(''); - expect(actualSvgChars).toEqual(test.expected[0] as string); - expect(actualPath.getSubPaths().length).toEqual(test.expected[1] as number); - }); - } - }); - - describe('determine whether subpaths are open or closed', () => { - const TESTS = [ - ['M 0 0 L 10 10 L 20 20', 0, false], - ['M 0 0 L 10 10 L 20 20 Z', 0, true], - ['M 5 5 h 10 v 10 h -10 v -10', 0, true], - ['M 5 5 h 10 v 10 h -10 v -10 M 15 15 L 10 10 L 5 5', 0, true], - ['M 5 5 h 10 v 10 h -10 v -10 M 15 15 L 10 10 L 5 5', 1, false], - ]; - - for (const test of TESTS) { - it(`subpath #${test[1]} in '${test[0]}' is ${test[2] ? 'closed' : 'open'}`, () => { - const path = new Path(test[0] as string); - expect(path.getSubPaths()[test[1] as number].isClosed()).toEqual(test[2] as boolean); - }); - } - }); - - describe('mutating Path objects', () => { - it('command IDs persist correctly after mutations', () => { - const totalIds = new Set(); - const extractPathIdsFn = (p: Path, expectedSize: number, expectedTotalSize: number) => { - const ids = p.getCommands().map(cmd => cmd.id); - ids.forEach(id => totalIds.add(id)); - expect(new Set(ids).size).toEqual(expectedSize); - expect(totalIds.size).toEqual(expectedTotalSize); - }; - - // Creating a new path generates 4 new ids. - let path = new Path('M 0 0 L 0 0 L 0 0 L 0 0'); - extractPathIdsFn(path, 4, 4); - - // Reversing/shifting an existing path generates no new ids. - path = path - .mutate() - .shiftSubPathBack(0) - .reverseSubPath(0) - .shiftSubPathForward(0) - .build(); - extractPathIdsFn(path, 4, 4); - - // Splitting an existing path generates no new ids. - path = path - .mutate() - .splitCommand(0, 2, 0.25, 0.5, 0.75) - .build(); - extractPathIdsFn(path, 7, 7); - - // Creating new paths generate new IDs. - path = new Path('M 0 0 L 0 0 L 0 0 L 0 0') - .mutate() - .shiftSubPathBack(0) - .build(); - extractPathIdsFn(path, 4, 11); - - path = new Path('M 0 0 L 0 0 L 0 0 L 0 0') - .mutate() - .reverseSubPath(0) - .build(); - extractPathIdsFn(path, 4, 15); - }); - - function makeTest(actual: string, ops: string, expected: string) { - return { actual, ops, expected }; - } - - const MUTATION_TESTS = [ - // Reverse/shift commands. - makeTest('M 0 0 10 10 20 20', 'RV 0', 'M 20 20 10 10 0 0'), - makeTest('M 0 0 L 10 10 L 20 20 Z', 'RV 0', 'M 0 0 L 20 20 L 10 10 L 0 0'), - makeTest('M 19 11 L 5 11 L 5 13 L 19 13 Z', 'RV 0', 'M 19 11 L 19 13 L 5 13 L 5 11 L 19 11'), - makeTest( - 'M 19 11 L 19 13 L 5 13 L 5 11 L 19 11', - 'RV 0', - 'M 19 11 L 5 11 L 5 13 L 19 13 L 19 11', - ), - makeTest('M 19 11 L 5 11 L 5 13 L 19 13 Z', 'RV 0 RV 0', 'M 19 11 L 5 11 L 5 13 L 19 13 Z'), - makeTest('M 19 11 L 5 11 L 5 13 L 19 13 Z', 'SF 0', 'M 5 11 L 5 13 L 19 13 L 19 11 L 5 11'), - makeTest('M 19 11 L 5 11 L 5 13 L 19 13 Z', 'SB 0 SF 0', 'M 19 11 L 5 11 L 5 13 L 19 13 Z'), - makeTest( - 'M 19 11 C 19 11 5 11 5 11 C 5 11 5 13 5 13 L 19 13 L 19 11', - 'RV 0', - 'M 19 11 L 19 13 L 5 13 C 5 13 5 11 5 11 C 5 11 19 11 19 11', - ), - makeTest( - 'M 5 13 L 8 13 L 20 13 L 20 11 L 20 11 L 8 11 L 5 11 L 4 12 L 5 13', - 'SB 0 SF 0', - 'M 5 13 L 8 13 L 20 13 L 20 11 L 20 11 L 8 11 L 5 11 L 4 12 L 5 13', - ), - makeTest( - 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z', - 'SIH 0 4 SFSP 0 1 4 SIH 0 4 SFSP 0 4 7', - 'M 20 11 L 7.83 11 L 8 8 L 4 12 L 8 16 L 7.83 13 L 20 13 L 20 11 L 20 11 M 8 16 L 12 20 L 13.41 18.59 ' + - 'L 7.83 13 L 8 16 M 7.83 11 L 13.42 5.41 L 12 4 L 8 8 L 7.83 11', - ), - makeTest( - 'M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z', - 'SIH 0 4 SFSP 0 1 4 SIH 0 4 SFSP 0 4 7 SB 0 SF 0', - 'M 20 11 L 7.83 11 L 8 8 L 4 12 L 8 16 L 7.83 13 L 20 13 L 20 11 L 20 11 M 8 16 L 12 20 L 13.41 18.59 ' + - 'L 7.83 13 L 8 16 M 7.83 11 L 13.42 5.41 L 12 4 L 8 8 L 7.83 11', - ), - // Split commands. - makeTest('M 0 0 L 10 10 L 20 20', 'S 0 1 0.5', 'M 0 0 L 5 5 L 10 10 L 20 20'), - makeTest('M 0 0 L 10 10 L 20 20', 'SIH 0 1', 'M 0 0 L 5 5 L 10 10 L 20 20'), - makeTest('M 0 0 L 5 5 L 10 10 L 20 20', 'SIH 0 2', 'M 0 0 L 5 5 L 7.5 7.5 L 10 10 L 20 20'), - makeTest('M 0 0 L 10 10 L 20 20', 'SIH 0 2', 'M 0 0 L 10 10 L 15 15 L 20 20'), - makeTest('M 0 0 L 10 10 L 20 20', 'RV 0 SIH 0 1', 'M 20 20 L 15 15 L 10 10 L 0 0'), - makeTest( - 'M 20 22 L 4 22 L 4 2 L 6 2 L 6 14 L 8 14 L 8 2 L 10 2 L 10 14 Z', - 'S 0 2 0.5', - 'M 20 22 L 4 22 L 4 12 L 4 2 L 6 2 L 6 14 L 8 14 L 8 2 L 10 2 L 10 14 Z', - ), - makeTest( - 'M 5 11 L 5 13 L 19 13 L 19 11 L 5 11', - 'SIH 0 4 S 0 4 1 SB 0 SB 0 SB 0 SB 0 SIH 0 6 US 0 2 S 0 5 1 US 0 2', - 'M 19 13 L 19 11 L 5 11 L 5 13 L 12 13 L 12 13 L 19 13', - ), - makeTest( - 'M 5 11 L 5 13 L 19 13 L 19 11 L 5 11', - 'SIH 0 2 SIH 0 2 SF 0 SF 0 SF 0 US 0 6 SIH 0 3 US 0 6 SIH 0 4', - 'M 5 13 L 19 13 L 19 11 L 12 11 L 8.5 11 L 5 11 L 5 13', - ), - // Split at t=0. - makeTest('M 0 0 L 0 10 L 10 10', 'S 0 2 0', 'M 0 0 L 0 10 L 0 10 L 10 10'), - // Split at t=1. - makeTest('M 0 0 L 0 10 L 10 10', 'S 0 2 1', 'M 0 0 L 0 10 L 10 10 L 10 10'), - // Split 0-length path. - makeTest('M 0 0 L 0 0', 'S 0 1 0 S 0 1 0.5 S 0 1 1', 'M 0 0 L 0 0 L 0 0 L 0 0 L 0 0'), - makeTest( - 'M 0 0 L 0 10 L 10 10 L 10 0 L 0 0', - 'S 0 2 0.25 0.5', - 'M 0 0 L 0 10 L 2.5 10 L 5 10 L 10 10 L 10 0 L 0 0', - ), - makeTest( - 'M 4 4 L 4 20 L 20 20 L 20 4 L 4 4', - 'SIH 0 4 SB 0', - 'M 12 4 L 4 4 L 4 20 L 20 20 L 20 4 L 12 4', - ), - makeTest( - 'M 4 4 L 4 20 L 20 20 L 20 4 L 4 4', - 'SIH 0 4 SB 0 S 0 5 0.25 0.5 0.75', - 'M 12 4 L 4 4 L 4 20 L 20 20 L 20 4 L 18 4 L 16 4 L 14 4 L 12 4', - ), - // Split closepath command. - makeTest('M 0 0 L 0 10 L 10 10 Z', 'S 0 3 0.5', 'M 0 0 L 0 10 L 10 10 L 5 5 Z'), - // Split in half closepath command. - makeTest('M 0 0 L 0 10 L 10 10 Z', 'SIH 0 3', 'M 0 0 L 0 10 L 10 10 L 5 5 Z'), - // Move sub paths. - makeTest('M 0 0 L 0 0 L 1 1', 'M 0 0', 'M 0 0 L 0 0 L 1 1'), - makeTest( - 'M 0 0 L 0 0 L 0 0 M 1 1 L 1 1 L 1 1 M 2 2 L 2 2 L 2 2', - 'M 0 1', - 'M 1 1 L 1 1 L 1 1 M 0 0 L 0 0 L 0 0 M 2 2 L 2 2 L 2 2', - ), - makeTest( - 'M 0 0 L 0 0 L 1 1 M 1 1 L 1 1 L 1 1 M 2 2 L 2 2 L 2 2', - 'M 0 1', - 'M 1 1 L 1 1 L 1 1 M 0 0 L 0 0 L 1 1 M 2 2 L 2 2 L 2 2', - ), - makeTest( - 'M 0 0 L 0 0 L 1 1 M 1 1 L 2 1 L 3 1 L 1 1 M 2 2 L 4 2 L 8 2', - 'M 0 1', - 'M 1 1 L 2 1 L 3 1 L 1 1 M 0 0 L 0 0 L 1 1 M 2 2 L 4 2 L 8 2', - ), - makeTest( - 'M 0 0 L 0 0 L 1 1 M 1 1 L 2 1 L 3 1 L 1 1 M 2 2 L 4 2 L 8 2', - 'M 0 1 M 1 0 M 1 2 M 2 1', - 'M 0 0 L 0 0 L 1 1 M 1 1 L 2 1 L 3 1 L 1 1 M 2 2 L 4 2 L 8 2', - ), - makeTest( - 'M 0 0 L 0 0 L 1 1 M 1 1 L 2 1 L 3 1 L 1 1 M 2 2 L 4 2 L 8 2', - 'M 0 1 RV 0', - 'M 1 1 L 3 1 L 2 1 L 1 1 M 0 0 L 0 0 L 1 1 M 2 2 L 4 2 L 8 2', - ), - makeTest( - 'M 0 0 L 0 0 L 1 1 M 1 1 L 2 1 L 3 1 L 1 1 M 2 2 L 4 2 L 8 2', - 'M 0 1 RV 0 SF 0', - 'M 3 1 L 2 1 L 1 1 L 3 1 M 0 0 L 0 0 L 1 1 M 2 2 L 4 2 L 8 2', - ), - makeTest( - 'M 0 0 L 0 0 L 1 1 M 1 1 L 2 1 L 3 1 L 1 1 M 2 2 L 4 2 L 8 2', - 'M 0 1 RV 0 SF 0 SIH 0 1', - 'M 3 1 L 2.5 1 L 2 1 L 1 1 L 3 1 M 0 0 L 0 0 L 1 1 M 2 2 L 4 2 L 8 2', - ), - makeTest( - 'M 0 0 L 0 0 L 1 1 M 1 1 L 2 1 L 3 1 L 1 1 M 2 2 L 4 2 L 8 2', - 'M 0 1 RV 0 SF 0 SIH 0 1 SB 0 RV 0', - 'M 1 1 L 2 1 L 2.5 1 L 3 1 L 1 1 M 0 0 L 0 0 L 1 1 M 2 2 L 4 2 L 8 2', - ), - makeTest( - 'M 0 0 L 0 0 L 1 1 M 1 1 L 2 1 L 3 1 L 1 1 M 2 2 L 4 2 L 8 2', - 'M 0 1 RV 0 SF 0 SIH 0 1 SB 0 RV 0 M 2 0 M 2 0 M 2 1', - 'M 0 0 L 0 0 L 1 1 M 1 1 L 2 1 L 2.5 1 L 3 1 L 1 1 M 2 2 L 4 2 L 8 2', - ), - makeTest( - 'M 0 0 L 0 0 L 1 1 M 1 1 L 2 1 L 3 1 L 1 1 M 2 2 L 4 2 L 8 2', - 'M 0 1 RV 0 SF 0 SIH 0 1 SB 0 RV 0 M 2 0 M 2 0 M 2 1 US 1 2', - 'M 0 0 L 0 0 L 1 1 M 1 1 L 2 1 L 3 1 L 1 1 M 2 2 L 4 2 L 8 2', - ), - makeTest( - 'M 1 1 L 2 2 L 3 3 M 10 10 L 20 20 L 30 30', - 'M 0 1 AC 3 4 2', - 'M 10 10 L 20 20 L 30 30 M 1 1 L 2 2 L 3 3 M 3 4 L 3 4', - ), - makeTest( - 'M 0 0 L 0 0 M 1 1 L 1 1 M 2 2 L 2 2 M 3 3 L 3 3 M 4 4 L 4 4 L 4 4', - 'M 1 4', - 'M 0 0 L 0 0 M 2 2 L 2 2 M 3 3 L 3 3 M 4 4 L 4 4 L 4 4 M 1 1 L 1 1', - ), - makeTest( - 'M 9 4 C 9 2.89 9.89 2 11 2 C 12.11 2 13 2.89 13 4 C 13 5.11 12.11 6 11 6 C 9.89 6 9 5.11 9 4 Z ' + - 'M 16 13 C 16 14.333 16 15.667 16 17 C 15 17 14 17 13 17 C 13 18.667 13 20.333 13 22 C 12 22 11 22 10 22 ' + - 'C 10 20.333 10 18.667 10 17 C 9.333 17 8.667 17 8 17 C 8 14.667 8 12.333 8 10 C 8 8.34 9.34 7 11 7 C 12.66 7 14 8.34 14 10 ' + - 'C 15.17 10.49 15.99 11.66 16 13 L 16 13 M 15 5.5 C 15 5.5 15 5.5 15 5.5 C 15 5.5 15 5.5 15 5.5 C 15 5.5 15 5.5 15 5.5 ' + - 'C 15 5.5 15 5.5 15 5.5 L 15 5.5 M 19.5 9.5 C 19.5 9.5 19.5 9.5 19.5 9.5 C 19.5 9.5 19.5 9.5 19.5 9.5 ' + - 'C 19.5 9.5 19.5 9.5 19.5 9.5 C 19.5 9.5 19.5 9.5 19.5 9.5 L 19.5 9.5 M 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 ' + - 'C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 ' + - 'C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 ' + - 'C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 ' + - 'C 11.99 16.24 11.99 16.24 11.99 16.24 L 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 ' + - 'C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 L 11.99 16.24', - 'M 1 4', - 'M 9 4 C 9 2.89 9.89 2 11 2 C 12.11 2 13 2.89 13 4 C 13 5.11 12.11 6 11 6 C 9.89 6 9 5.11 9 4 Z ' + - 'M 15 5.5 C 15 5.5 15 5.5 15 5.5 C 15 5.5 15 5.5 15 5.5 C 15 5.5 15 5.5 15 5.5 ' + - 'C 15 5.5 15 5.5 15 5.5 L 15 5.5 M 19.5 9.5 C 19.5 9.5 19.5 9.5 19.5 9.5 C 19.5 9.5 19.5 9.5 19.5 9.5 ' + - 'C 19.5 9.5 19.5 9.5 19.5 9.5 C 19.5 9.5 19.5 9.5 19.5 9.5 L 19.5 9.5 M 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 ' + - 'C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 ' + - 'C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 ' + - 'C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 ' + - 'C 11.99 16.24 11.99 16.24 11.99 16.24 L 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 ' + - 'C 11.99 16.24 11.99 16.24 11.99 16.24 C 11.99 16.24 11.99 16.24 11.99 16.24 L 11.99 16.24' + - 'M 16 13 C 16 14.333 16 15.667 16 17 C 15 17 14 17 13 17 C 13 18.667 13 20.333 13 22 C 12 22 11 22 10 22 ' + - 'C 10 20.333 10 18.667 10 17 C 9.333 17 8.667 17 8 17 C 8 14.667 8 12.333 8 10 C 8 8.34 9.34 7 11 7 C 12.66 7 14 8.34 14 10 ' + - 'C 15.17 10.49 15.99 11.66 16 13 L 16 13', - ), - // Convert/unconvert commands. - makeTest('M 0 0 L 3 3', 'CV 0 1 C', 'M 0 0 C 1 1 2 2 3 3'), - makeTest('M 0 0 L 3 3', 'CV 0 1 C UCV 0', 'M 0 0 L 3 3'), - // Transform paths. - makeTest('M-4-8h8v16h-8v-16', 'T translate 4 8', 'M0 0h8v16h-8v-16'), - makeTest('M-4-8h8v16h-8v-16', 'T rotate 90', 'M 8 -4 v 8 h -16 v -8 h 16'), - makeTest('M-4-8h8v16h-8v-16', 'T rotate 180', 'M 4 8 h -8 v -16 h 8 v 16'), - makeTest('M-4-8h8v16h-8v-16', 'T scale 0.5 0.5', 'M -2 -4 h 4 v 8 h -4 v -8'), - makeTest( - 'M-4-8h8v16h-8v-16', - 'T translate 1 2 scale 2 3 rotate 34 translate 3 4 RT', - 'M-4-8h8v16h-8v-16', - ), - // Add/delete collapsing sub paths. - makeTest('M 0 0 L 3 3', 'AC 5 5 10', `M 0 0 L 3 3 M 5 5${' L 5 5'.repeat(9)}`), - makeTest( - 'M 0 0 L 3 3', - 'AC 5 5 5 AC 3 4 6', - `M 0 0 L 3 3 M 5 5${' L 5 5'.repeat(4)} M 3 4${' L 3 4'.repeat(5)}`, - ), - makeTest( - 'M 1 1 L 3 3 L 1 1', - 'AC 5 5 5 AC 3 4 6 M 0 1', - `M 5 5${' L 5 5'.repeat(4)} M 1 1 L 3 3 L 1 1 M 3 4${' L 3 4'.repeat(5)}`, - ), - makeTest('M 1 1 L 3 3 L 1 1', 'AC 5 5 5 AC 3 4 6 M 0 1 DC', `M 1 1 L 3 3 L 1 1`), - makeTest('M 1 1 L 3 3 L 1 1', 'AC 5 5 5 AC 3 4 6 M 0 1 RT', `M 1 1 L 3 3 L 1 1`), - makeTest( - 'M 1 1 L 2 2 L 3 3 M 10 10 L 20 20 L 30 30', - 'AC 3 4 2', - 'M 1 1 L 2 2 L 3 3 M 10 10 L 20 20 L 30 30 M 3 4 L 3 4', - ), - // Split/unsplit stroked sub paths. - makeTest( - 'M 0 0 L 1 1 L 2 2 L 3 3 L 4 4 L 5 5 M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - 'SSSP 0 1', - 'M 0 0 L 1 1 M 1 1 L 2 2 L 3 3 L 4 4 L 5 5 M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - ), - makeTest( - 'M 0 0 L 1 1 L 2 2 L 3 3 L 4 4 L 5 5 M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - 'SSSP 0 1 DSSP 0', - 'M 0 0 L 1 1 L 2 2 L 3 3 L 4 4 L 5 5 M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - ), - makeTest( - 'M 0 0 L 1 1 L 2 2 L 3 3 L 4 4 L 5 5 M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - 'SSSP 0 1 M 0 1', - 'M 1 1 L 2 2 L 3 3 L 4 4 L 5 5 M 0 0 L 1 1 M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - ), - makeTest( - 'M 0 0 L 1 1 L 2 2 L 3 3 L 4 4 L 5 5 M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - 'SSSP 0 1 M 0 1 DSSP 1', - 'M 0 0 L 1 1 L 2 2 L 3 3 L 4 4 L 5 5 M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - ), - makeTest( - 'M 0 0 L 1 1 L 2 2 L 3 3 L 4 4 L 5 5 M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - 'SSSP 0 1 DSSP 1', - 'M 0 0 L 1 1 L 2 2 L 3 3 L 4 4 L 5 5 M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - ), - makeTest( - 'M 0 0 L 1 1 L 2 2 L 3 3 L 4 4 L 5 5 M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - 'SSSP 0 1 SSSP 2 2', - 'M 0 0 L 1 1 M 1 1 L 2 2 L 3 3 L 4 4 L 5 5 M 0 0 L 10 10 L 20 20 M 20 20 L 30 30 L 40 40 L 50 50', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - 'SIH 0 4 SSSP 0 4', - 'M 0 0 L 10 10 L 20 20 L 30 30 L 35 35 M 35 35 L 40 40 L 50 50', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - 'SIH 0 4 SSSP 0 4 M 0 1 S 0 1 0.6', - 'M 35 35 L 38 38 L 40 40 L 50 50 M 0 0 L 10 10 L 20 20 L 30 30 L 35 35', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - 'SIH 0 4 SSSP 0 4 M 0 1 S 0 1 0.6 RV 0 S 0 2 0.5', - 'M 50 50 L 40 40 L 39 39 L 38 38 L 35 35 M 0 0 L 10 10 L 20 20 L 30 30 L 35 35', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - 'SIH 0 4 SSSP 0 4 M 0 1 S 0 1 0.6 RV 0 S 0 2 0.5 RT', - 'M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - 'SIH 0 4 SSSP 0 4 M 0 1 S 0 1 0.6 RV 0 S 0 2 0.5 RT ' + - 'SIH 0 4 SSSP 0 4 M 0 1 S 0 1 0.6 RV 0 S 0 2 0.5 RT ' + - 'SIH 0 4 SSSP 0 4 M 0 1 S 0 1 0.6 RT', - 'M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30 L 40 40 L 50 50', - 'SIH 0 4 SSSP 0 4 M 0 1 S 0 1 0.6 RV 0 S 0 2 0.5 RT ' + - 'SIH 0 4 SSSP 0 4 M 0 1 S 0 1 0.6 RV 0 S 0 2 0.5 RT ' + - 'SIH 0 4 SSSP 0 4 M 0 1 S 0 1 0.6 RT ' + - 'SIH 0 4 SSSP 0 4', - 'M 0 0 L 10 10 L 20 20 L 30 30 L 35 35 M 35 35 L 40 40 L 50 50', - ), - makeTest( - 'M 50 50 L 40 40 L 39 39 L 38 38 L 35 35 M 0 0 L 10 10 L 20 20 L 30 30 L 35 35', - 'SSSP 0 3', - 'M 50 50 L 40 40 L 39 39 L 38 38 M 38 38 L 35 35 M 0 0 L 10 10 L 20 20 L 30 30 L 35 35', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30', - 'RV 0 SSSP 0 2', - 'M 30 30 L 20 20 L 10 10 M 10 10 L 0 0', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30', - 'RV 0 SSSP 0 2 DSSP 0', - 'M 30 30 L 20 20 L 10 10 L 0 0', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30', - 'RV 0 SSSP 0 2 DSSP 1', - 'M 30 30 L 20 20 L 10 10 L 0 0', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30', - 'RV 0 SSSP 0 2 RV 1', - 'M 30 30 L 20 20 L 10 10 M 0 0 L 10 10', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30', - 'RV 0 SSSP 0 2 RV 0 RV 1', - 'M 10 10 L 20 20 L 30 30 M 0 0 L 10 10', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30', - 'RV 0 SSSP 0 2 RV 0 RV 1 S 1 1 0.7', - 'M 10 10 L 20 20 L 30 30 M 0 0 L 7 7 L 10 10', - ), - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30', - 'RV 0 SSSP 0 2 RV 0 RV 1 S 1 1 0.7', - 'M 10 10 L 20 20 L 30 30 M 0 0 L 7 7 L 10 10', - ), - // Deleting nonexistent collapsing subpaths after splitting a stroked subpath has no effect. - makeTest( - 'M 0 0 L 10 10 L 20 20 L 30 30', - 'SSSP 0 2 AC 5 5 5 DC DC', - 'M 0 0 L 10 10 L 20 20 M 20 20 L 30 30', - ), - makeTest( - 'M 0 0 L 1 1 L 2 2', - 'RV 0 S 0 2 0.8 S 0 2 0.25 S 0 3 0.75 SSSP 0 3', - 'M 2 2 L 1 1 L 0.8 0.8 L 0.35 0.35 M 0.35 0.35 L 0.2 0.2 L 0 0', - ), - makeTest( - 'M 7 8 C 7 2 16 2 16 8 C 16 10 14 12 12 14', - 'RV 0 SIH 0 2 SSSP 0 2', - 'M 12 14 C 14 12 16 10 16 8 C 16 5 13.75 3.5 11.5 3.5 M 11.5 3.5 C 9.25 3.5 7 5 7 8', - ), - makeTest( - 'M 7 8 C 7 2 16 2 16 8 C 16 10 14 12 12 14', - 'RV 0 SIH 0 2 SSSP 0 2 RT', - 'M 7 8 C 7 2 16 2 16 8 C 16 10 14 12 12 14', - ), - makeTest( - 'M 1 1 L 2 1 L 2 2 M 5 5 L 5 10 L 10 10 L 10 5 L 5 5', - 'RV 1 SF 1 SIH 1 2 SSSP 1 2', - 'M 1 1 L 2 1 L 2 2 M 10 5 L 10 10 L 7.5 10 M 7.5 10 L 5 10 L 5 5 L 10 5', - ), - // Split/unsplit filled sub paths. - makeTest( - 'M 8 5 L 8 19 L 19 12 L 8 5', - 'SIH 0 1 SFSP 0 1 3', - 'M 8 5 L 8 12 L 19 12 L 8 5 M 8 12 L 8 19 L 19 12 L 8 12', - ), - makeTest( - 'M 8 5 L 8 19 L 19 12 L 8 5', - 'SIH 0 1 SFSP 0 3 1', - 'M 8 5 L 8 12 L 19 12 L 8 5 M 8 12 L 8 19 L 19 12 L 8 12', - ), - makeTest( - 'M 8 5 L 8 19 L 19 12 L 8 5', - 'SIH 0 1 SIH 0 4 SFSP 0 1 4', - 'M 8 5 L 8 12 L 13.5 8.5 L 8 5 M 8 12 L 8 19 L 19 12 L 13.5 8.5 L 8 12', - ), - makeTest( - 'M 8 5 L 8 19 L 19 12 Z', - 'SIH 0 1 SIH 0 4 SFSP 0 1 4', - 'M 8 5 L 8 12 L 13.5 8.5 L 8 5 M 8 12 L 8 19 L 19 12 L 13.5 8.5 L 8 12', - ), - makeTest( - 'M 8 5 L 8 19 L 19 12 Z', - 'AC 5 5 1 S 0 1 0.4 S 0 4 0.6 SFSP 0 1 4 UCV 0 UCV 1 UCV 2 DC', - 'M 8 5 L 8 10.6 L 12.4 7.8 L 8 5 M 8 10.6 L 8 19 L 19 12 L 12.4 7.8 L 8 10.6', - ), - makeTest( - 'M 8 5 L 8 19 L 19 12 L 8 5', - 'SIH 0 1 SFSP 0 1 3 S 0 3 0.4', - `M 8 5 L 8 12 L 19 12 L ${lerp(19, 8, 0.4)} ${lerp( - 12, - 5, - 0.4, - )} L 8 5 M 8 12 L 8 19 L 19 12 L 8 12`, - ), - makeTest( - 'M 8 5 L 8 19 L 19 12 L 8 5', - 'SIH 0 1 S 0 3 1 SFSP 0 1 3', - 'M 8 5 L 8 12 L 19 12 L 19 12 L 8 5 M 8 12 L 8 19 L 19 12 L 8 12', - ), - makeTest( - 'M 8 5 L 8 19 L 19 12 L 8 5', - 'SIH 0 1 S 0 3 1 SFSP 0 1 3', - 'M 8 5 L 8 12 L 19 12 L 19 12 L 8 5 M 8 12 L 8 19 L 19 12 L 8 12', - ), - makeTest( - 'M0 0L0 0v10h10v-10h-10', - 'SFSP 0 1 3', - 'M 0 0 L 0 0 L 10 10 L 10 0 L 0 0 M 0 0 L 0 10 L 10 10 L 0 0', - ), - makeTest('M0 0L0 0v10h10v-10h-10', 'SFSP 0 1 3 DSSP 0', 'M0 0L0 0v10h10v-10h-10'), - makeTest('M0 0L0 0v10h10v-10h-10', 'SFSP 0 1 3 SF 1 RV 1 DSSP 0', 'M0 0L0 0v10h10v-10h-10'), - makeTest( - 'M0 0L0 0v10h10v-10h-10', - 'SFSP 0 1 3 RV 0 SF 1 SF 1 DSSP 1', - 'M0 0L0 0v10h10v-10h-10', - ), - makeTest( - 'M 0 0 L 0 10 L 10 10 L 10 0 L 0 0', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4', - 'M 0 0 L 0 5 L 10 5 L 10 0 L 0 0 M 0 5 L 0 10 L 10 10 L 10 5 L 0 5', - ), - makeTest( - 'M 0 0 L 0 10 L 10 10 L 10 0 L 0 0', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 SIH 1 4 SIH 1 2 SFSP 1 2 5', - 'M 0 0 L 0 5 L 10 5 L 10 0 L 0 0 M 0 5 L 0 10 L 5 10 L 5 5 L 0 5 M 5 10 L 10 10 L 10 5 L 5 5 L 5 10', - ), - // Delete sub path split segment. - makeTest( - 'M 0 0 L 0 10 L 10 10 L 10 0 L 0 0', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 DFSPS 0 2', - 'M 0 0 L 0 10 L 10 10 L 10 0 L 0 0', - ), - makeTest( - 'M 0 0 L 0 10 L 10 10 L 10 0 L 0 0', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 DFSPS 1 4', - 'M 0 0 L 0 10 L 10 10 L 10 0 L 0 0', - ), - makeTest( - 'M 0 0 L 0 10 L 10 10 L 10 0 L 0 0', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 SIH 1 4 SIH 1 2 SFSP 1 2 5 DFSPS 1 3', - 'M 0 0 L 0 5 L 10 5 L 10 0 L 0 0 M 0 5 L 0 10 L 10 10 L 10 5 L 0 5', - ), - makeTest( - 'M 0 0 L 0 10 L 10 10 L 10 0 L 0 0', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 SIH 1 4 SIH 1 2 SFSP 1 2 5 DFSPS 2 4', - 'M 0 0 L 0 5 L 10 5 L 10 0 L 0 0 M 0 5 L 0 10 L 10 10 L 10 5 L 0 5', - ), - makeTest( - 'M 0 0 L 0 15 L 15 15 L 15 0 L 0 0', - `S 0 3 ${2 / 3} S 0 3 0.5 S 0 1 ${2 / 3} S 0 1 0.5 SFSP 0 1 6`, - `M 0 0 L 0 5 L 15 5 L 15 0 L 0 0 M 0 5 L 0 10 L 0 15 L 15 15 L 15 10 L 15 5 L 0 5`, - ), - makeTest( - 'M 0 0 L 0 15 L 15 15 L 15 0 L 0 0', - `S 0 3 ${2 / 3} S 0 3 0.5 S 0 1 ${2 / 3} S 0 1 0.5 SFSP 0 1 6 SFSP 1 1 4`, - `M 0 0 L 0 5 L 15 5 L 15 0 L 0 0 M 0 5 L 0 10 L 15 10 L 15 5 L 0 5 M 0 10 L 0 15 L 15 15 L 15 10 L 0 10`, - ), - makeTest( - 'M 0 0 L 0 15 L 15 15 L 15 0 L 0 0', - `S 0 3 ${2 / 3} S 0 3 0.5 S 0 1 ${2 / 3} S 0 1 0.5 SFSP 0 1 6 SFSP 1 1 4 DFSPS 1 2`, - `M 0 0 L 0 5 L 15 5 L 15 0 L 0 0 M 0 5 L 0 15 L 15 15 L 15 5 L 0 5`, - ), - makeTest( - 'M 0 0 L 0 15 L 15 15 L 15 0 L 0 0', - `S 0 3 ${2 / 3} S 0 3 0.5 S 0 1 ${2 / 3} S 0 1 0.5 SFSP 0 1 6 SFSP 1 1 4 DFSPS 2 4`, - `M 0 0 L 0 5 L 15 5 L 15 0 L 0 0 M 0 5 L 0 15 L 15 15 L 15 5 L 0 5`, - ), - makeTest( - 'M 18 19 L 18 5 L 14 5 L 14 19 L 18 19', - 'S 0 3 0.5 S 0 1 0.5 SFSP 0 1 4', - 'M 18 19 L 18 12 L 14 12 L 14 19 L 18 19 M 18 12 L 18 5 L 14 5 L 14 12 L 18 12', - ), - makeTest( - 'M 18 19 L 18 5 L 14 5 L 14 19 L 18 19 M 10 19 L 10 5 L 6 5 L 6 19 L 10 19', - 'S 0 3 0.5 S 0 1 0.5 SFSP 0 1 4', - 'M 18 19 L 18 12 L 14 12 L 14 19 L 18 19 M 18 12 L 18 5 L 14 5 L 14 12 L 18 12 M 10 19 L 10 5 L 6 5 L 6 19 L 10 19', - ), - makeTest( - 'M 8 5 L 8 19 L 19 12 L 8 5', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 SB 0 SB 0 SIH 0 2 SIH 0 1 SFSP 0 1 3 RT', - 'M 8 5 L 8 19 L 19 12 L 8 5', - ), - makeTest( - 'M 0 0 h 20 v 20 h -20 v -20', - 'S 0 3 0.75 S 0 1 0.25 SFSP 0 1 4 S 1 3 0.5 S 1 1 0.5 SFSP 1 1 4', - 'M 0 0 L 5 0 L 5 20 L 0 20 L 0 0 M 5 0 L 12.5 0 L 12.5 20 L 5 20 L 5 0 M 12.5 0 L 20 0 L 20 20 L 12.5 20 L 12.5 0', - ), - makeTest( - 'M 0 0 h 20 v 20 h -20 v -20', - 'S 0 3 0.75 S 0 1 0.25 SFSP 0 1 4 S 1 3 0.5 S 1 1 0.5 SFSP 1 1 4 DFSPS 1 2', - 'M 0 0 L 5 0 L 5 20 L 0 20 L 0 0 M 5 0 L 20 0 L 20 20 L 5 20 L 5 0', - ), - makeTest( - 'M 0 0 h 20 v 20 h -20 v -20', - 'S 0 3 0.75 S 0 1 0.25 SFSP 0 1 4 S 1 3 0.5 S 1 1 0.5 SFSP 1 1 4 DFSPS 2 4', - 'M 0 0 L 5 0 L 5 20 L 0 20 L 0 0 M 5 0 L 20 0 L 20 20 L 5 20 L 5 0', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - `SIH 0 3 SIH 0 1 SFSP 0 1 4 S 1 4 0.625 S 1 1 0.75 SFSP 1 1 5 SIH 1 3 SIH 1 2 SFSP 1 2 4 DFSPS 3 4`, - 'M 4 4 h 16 v 16 h -16 v -16', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 S 1 4 0.625 S 1 1 0.75 SFSP 1 1 5 SIH 1 3 SIH 1 2 SFSP 1 2 4 DFSPS 0 1', - 'M 4 4 h 16 v 16 h -16 v -16', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 S 1 4 0.625 SFSP 1 1 4 SIH 1 3 SIH 1 2 SFSP 1 2 4 SFSP 3 1 3 DFSPS 3 2', - 'M4 4L12 4L12 20L4 20L4 4M12 4L20 4L16 7L12 7L12 4M16 7L12 10L12 7L16 7M20 4L20 20L12 20L12 10L20 4', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 SIH 0 4 SIH 0 3 SFSP 0 3 5 DFSPS 0 2', - 'M 4 4 L 20 4 L 20 20 L 8 20 L 4 12 L 4 4 M 8 20 L 4 20 L 4 12 L 8 20', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - 'SIH 0 4 SIH 0 1 SFSP 0 1 5 SIH 0 2 SFSP 0 2 4', - 'M 4 4 L 12 4 L 8 8 L 4 4 M 8 8 L 4 12 L 4 4 L 8 8 M 12 4 L 20 4 L 20 20 L 4 20 L 4 12 L 12 4', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - 'SIH 0 4 SIH 0 1 SFSP 0 1 5 SIH 0 2 SFSP 0 2 4 SIH 0 1 SFSP 0 1 3', - 'M 4 4 L 8 4 L 8 8 L 4 4 M 8 4 L 12 4 L 8 8 L 8 4 M 8 8 L 4 12 L 4 4 L 8 8 M 12 4 L 20 4 L 20 20 L 4 20 L 4 12 L 12 4', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - 'SIH 0 4 SIH 0 1 SFSP 0 1 5 SIH 0 2 SFSP 0 2 4 SIH 0 1 SFSP 0 1 3 DFSPS 0 2', - 'M 4 4 L 12 4 L 8 8 L 4 4 M 8 8 L 4 12 L 4 4 L 8 8 M 12 4 L 20 4 L 20 20 L 4 20 L 4 12 L 12 4', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - 'SIH 0 4 SIH 0 1 SFSP 0 1 5 SIH 0 2 SFSP 0 2 4 SIH 0 1 SFSP 0 1 3 DFSPS 1 3', - 'M 4 4 L 12 4 L 8 8 L 4 4 M 8 8 L 4 12 L 4 4 L 8 8 M 12 4 L 20 4 L 20 20 L 4 20 L 4 12 L 12 4', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - 'SIH 0 4 SIH 0 1 SFSP 0 1 5 SIH 0 2 SFSP 0 2 4 SIH 0 1 SFSP 0 1 3 DFSPS 0 3', - 'M 4 4 L 8 4 L 8 8 L 4 12 L 4 4 M 8 4 L 12 4 L 8 8 L 8 4 M 12 4 L 20 4 L 20 20 L 4 20 L 4 12 L 12 4', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - 'SIH 0 4 SIH 0 2 SFSP 0 2 5 SIH 0 3 SIH 0 1 SFSP 0 1 4 SIH 1 3 SIH 1 1 SFSP 1 1 4 DFSPS 0 2', - 'M 4 4 L 16 4 L 16 12 L 4 12 L 4 4 M 16 4 L 20 4 L 20 12 L 16 12 L 16 4 M 20 12 L 20 20 L 4 20 L 4 12 L 20 12', - ), - makeTest( - 'M 4 4 v 16 h 16 v -16 h -16', - 'SIH 0 4 SIH 0 1 SFSP 0 1 5 SIH 1 3 SFSP 1 3 5', - 'M 4 4 L 4 12 L 12 4 L 4 4 M 4 12 L 4 20 L 20 20 L 20 12 L 12 4 L 4 12 M 20 12 L 20 4 L 12 4 L 20 12', - ), - makeTest( - 'M 4 4 v 16 h 16 v -16 h -16', - 'SIH 0 4 SIH 0 1 SFSP 0 1 5 SIH 1 3 SFSP 1 3 5 DFSP 0 DFSP 0', - 'M 4 4 v 16 h 16 v -16 h -16', - ), - makeTest( - 'M 4 4 v 16 h 16 v -16 h -16', - 'SIH 0 4 SIH 0 1 SFSP 0 1 5 SIH 1 3 SFSP 1 3 5 DFSP 2 DFSP 0', - 'M 4 4 v 16 h 16 v -16 h -16', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 SIH 1 4 SIH 1 2 SFSP 1 2 5', - 'M 4 4 L 12 4 L 12 20 L 4 20 L 4 4 M 12 4 L 20 4 L 20 12 L 12 12 L 12 4 M 20 12 L 20 20 L 12 20 L 12 12 L 20 12', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 SIH 1 4 SIH 1 2 SFSP 1 2 5 DFSPS 2 4', - 'M 4 4 L 12 4 L 12 20 L 4 20 L 4 4 M 12 4 L 20 4 L 20 20 L 12 20 L 12 4', - ), - makeTest( - 'M 4 4 h 16 v 16 h -16 v -16', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 SIH 1 4 SIH 1 2 SFSP 1 2 5 DFSPS 2 3', - 'M 4 4 h 16 v 16 h -16 v -16', - ), - makeTest( - 'M8 5v14l11-7L8 5', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 SIH 1 4 SIH 1 2 SFSP 1 2 5 DFSP 0', - 'M8 5v14l11-7L8 5', - ), - makeTest( - 'M8 5v14l11-7L8 5', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 SIH 1 4 SIH 1 2 SFSP 1 2 5 DFSP 1', - 'M 8 5 L 8 12 L 13.5 8.5 L 8 5 M 8 12 L 8 19 L 19 12 L 13.5 8.5 L 8 12', - ), - makeTest( - 'M8 5v14l11-7L8 5', - 'SIH 0 3 SIH 0 1 SFSP 0 1 4 SIH 1 4 SIH 1 2 SFSP 1 2 5 DFSP 2', - 'M 8 5 L 8 12 L 13.5 8.5 L 8 5 M 8 12 L 8 19 L 19 12 L 13.5 8.5 L 8 12', - ), - makeTest( - 'M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 C 6 14.81 8.69 17.5 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5', - 'SFSP 0 0 2', - 'M 12 5.5 L 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5 M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 ' + - 'C 6 14.81 8.69 17.5 12 17.5 L 12 5.5', - ), - makeTest( - 'M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 C 6 14.81 8.69 17.5 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5', - 'SFSP 0 0 2 SFSP 1 0 1', - 'M 12 5.5 L 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5 M 12 5.5 L 6 11.5 ' + - 'C 6 14.81 8.69 17.5 12 17.5 L 12 5.5 M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 L 12 5.5', - ), - makeTest( - 'M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 C 6 14.81 8.69 17.5 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5', - 'SFSP 0 0 2 SFSP 1 0 1 DFSPS 2 2', - 'M 12 5.5 L 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5 M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 ' + - 'C 6 14.81 8.69 17.5 12 17.5 L 12 5.5', - ), - makeTest( - 'M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 C 6 14.81 8.69 17.5 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5', - 'SFSP 0 0 1 SFSP 0 1 3 SFSP 1 1 2', - 'M 12 5.5 L 6 11.5 L 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5 M 6 11.5 C 6 14.81 8.69 17.5 12 17.5 L 18 11.5 L 6 11.5 ' + - 'M 12 17.5 C 15.31 17.5 18 14.81 18 11.5 L 12 17.5 M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 L 12 5.5', - ), - makeTest( - 'M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 C 6 14.81 8.69 17.5 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5', - 'SFSP 0 0 1 SFSP 0 1 3 DFSP 0', - 'M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 C 6 14.81 8.69 17.5 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5', - ), - makeTest( - 'M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 C 6 14.81 8.69 17.5 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5', - 'SFSP 0 0 1 SFSP 0 1 3 DFSP 1 DFSP 0', - 'M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 C 6 14.81 8.69 17.5 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5', - ), - makeTest( - 'M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 C 6 14.81 8.69 17.5 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5', - 'SFSP 0 0 1 SFSP 0 1 3 DFSP 1 DFSP 1', - 'M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 C 6 14.81 8.69 17.5 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5', - ), - makeTest( - 'M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 C 6 14.81 8.69 17.5 12 17.5 C 15.31 17.5 18 14.81 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5', - 'SFSP 0 0 1 SFSP 0 1 3 SFSP 1 1 2 DFSP 0', - 'M 12 5.5 C 8.69 5.5 6 8.19 6 11.5 C 6 14.81 8.69 17.5 12 17.5 L 18 11.5 C 18 8.19 15.31 5.5 12 5.5 L 12 5.5 M 12 17.5 ' + - 'C 15.31 17.5 18 14.81 18 11.5 L 12 17.5', - ), - // TODO: determine if this is the right behavior - // makeTest( - // 'M 4 4 h 16 v 16 h -16 v -16', - // 'SIH 0 3 SIH 0 1 SFSP 0 1 4 SIH 1 3 DFSPS 0 2', - // 'M 4 4 h 16 v 16 h -4 h -12 v -16', - // ), - // TODO: add tests for shift offsets w/ split sub paths - // TODO: add more tests for compound paths w/ split sub paths - // TODO: better tests for multiple transforms at a time - // TODO: test that reversals and shifts still work after transforms - // TODO: test that splits and conversions and stuff still work after transforms - // TODO: test that reversals/shifts/splits/etc. are reverted properly, not just transforms - ]; - - for (const test of MUTATION_TESTS) { - it(`[${test.ops}] '${test.actual}' → '${test.expected}'`, () => { - checkPathsEqual(fromPathOpString(test.actual, test.ops), new Path(test.expected)); - }); - } - }); - - describe('assigning subpath IDs', () => { - const SUBPATH_ID_TESTS = [ - { - desc: 'id set on single-subpath path', - path: 'M 0 0 L 0 0 L 0 0 L 0 0 L 0 0 L 0 0', - expected: 1, - }, - { - desc: 'id set on two-subpath path', - path: 'M 0 0 L 0 0 M 0 0 L 0 0 L 0 0 L 0 0', - expected: 2, - }, - { - desc: 'id set on three-subpath path', - path: 'M 0 0 L 0 0 M 0 0 L 0 0 M 0 0 L 0 0', - expected: 3, - }, - ]; - - const countUniqueSubPathIdsFn = (pathString: string) => { - return new Set(new Path(pathString).getSubPaths().map(s => s.getId())).size; - }; - - for (const test of SUBPATH_ID_TESTS) { - it(test.desc, () => { - expect(countUniqueSubPathIdsFn(test.path)).toEqual(test.expected); - }); - } - - it('subpath IDs persist correctly after mutations', () => { - const path = new Path('M 0 0 L 0 0 M 0 0 L 0 0 M 0 0 L 0 0'); - const subPathIds = path.getSubPaths().map(s => s.getId()); - const updatedPath = path - .mutate() - .moveSubPath(0, 1) - .build(); - const updatedSubPathIds = updatedPath.getSubPaths().map(s => s.getId()); - expect(updatedSubPathIds).toEqual([subPathIds[1], subPathIds[0], subPathIds[2]]); - const revertedPath = updatedPath - .mutate() - .revert() - .build(); - const revertedSubPathIds = revertedPath.getSubPaths().map(s => s.getId()); - expect(revertedSubPathIds).toEqual(subPathIds); - }); - }); - - // TODO: add more projection tests for split subpaths - describe('#project', () => { - const TESTS_PROJECT: Array<{ - point: Point; - path: string | Path; - proj: ProjectionOntoPath; - subIdx?: number; - }> = [ - { - point: newPoint(5, 5), - path: 'M 0 0 L 10 10', - proj: { subIdx: 0, cmdIdx: 1, projection: { x: 5, y: 5, d: 0, t: 0.5 } }, - }, - { - point: newPoint(24, 12), - path: fromPathOpString('M 8 5 L 8 19 L 19 12 Z', 'SIH 0 2 S 0 1 0.5 SFSP 0 1 4 US 1 2'), - proj: { subIdx: 0, cmdIdx: 2, projection: { x: 19, y: 12, d: 5, t: 1 } }, - }, - { - point: newPoint(7, 16.9), - path: fromPathOpString('M 8 5 L 8 19 L 19 12 Z', 'S 0 1 0.5 SFSP 0 1 3'), - proj: { subIdx: 1, cmdIdx: 1, projection: { x: 8, y: 16.9, d: 1, t: 0.7 } }, - }, - { - point: newPoint(3, 12), - path: 'M 18 19 18 15 14 5 14 19 18 19 M 10 19 10 5 6 5 6 19 10 19', - proj: { subIdx: 1, cmdIdx: 3, projection: { x: 6, y: 12, d: 3, t: 0.5 } }, - }, - { - point: newPoint(3, 12), - path: 'M 18 19 18 15 14 5 14 19 18 19 M 10 19 10 5 6 5 6 19 10 19', - proj: { subIdx: 1, cmdIdx: 3, projection: { x: 6, y: 12, d: 3, t: 0.5 } }, - subIdx: 1, - }, - { - point: newPoint(21, 12), - path: 'M 18 19 18 15 14 5 14 19 18 19 M 10 19 10 5 6 5 6 19 10 19', - proj: { subIdx: 1, cmdIdx: 1, projection: { x: 10, y: 12, d: 11, t: 0.5 } }, - subIdx: 1, - }, - { - point: newPoint(4, 12), - path: 'M 20 22 L 4 22 L 4 2 L 6 2 L 6 14 L 8 14 L 8 2 L 10 2 L 10 14 Z', - proj: { subIdx: 0, cmdIdx: 2, projection: { x: 4, y: 12, d: 0, t: 0.5 } }, - }, - ]; - - TESTS_PROJECT.forEach(a => { - const point = a.point as Point; - const path = typeof a.path === 'string' ? new Path(a.path) : a.path; - it(`projecting '(${point.x},${ - point.y - })' onto '${path.getPathString()}' yields ${JSON.stringify(a.proj)}`, () => { - const result = path.project(point, a.subIdx); - result.projection.t = _.round(result.projection.t, 10); - expect(result).toEqual(a.proj as ProjectionOntoPath); - }); - }); - }); - - // TODO: add more projection tests for split subpaths - describe('#hitTest', () => { - const TESTS_HIT_TEST_FILL = [ - [newPoint(5, 5), 'M4 4h2v2h-2v-2', true], - [newPoint(5, 5), 'M4 4Q 5 4 6 4 Q6 5 6 6 Q5 6 4 6 Q4 5 4 4', true], - [newPoint(16, 7), 'M6 19h4V5H6v14zm8-14v14h4V5h-4z', true], - [newPoint(16, 12), 'M6 19h4V5H6v14zm8-14v14h4V5h-4z', true], - [newPoint(16, 17), 'M6 19h4V5H6v14zm8-14v14h4V5h-4z', true], - [newPoint(0, 0), 'M6 19h4V5H6v14zm8-14v14h4V5h-4z', false], - [newPoint(0, 12), 'M6 19h4V5H6v14zm8-14v14h4V5h-4z', false], - [newPoint(12, 12), 'M6 19h4V5H6v14zm8-14v14h4V5h-4z', false], - [newPoint(12, 0), 'M6 19h4V5H6v14zm8-14v14h4V5h-4z', false], - [newPoint(24, 24), 'M6 19h4V5H6v14zm8-14v14h4V5h-4z', false], - [newPoint(19, 20), 'M6 19h4V5H6v14zm8-14v14h4V5h-4z', false], - [newPoint(14, 10), fromPathOpString('M8 5L8 19L19 12L8 5', 'SIH 0 1 SFSP 0 1 3'), true], - [newPoint(17, 6), fromPathOpString('M8 5L8 19L19 12L8 5', 'SIH 0 1 SFSP 0 1 3'), false], - [newPoint(11, 9), fromPathOpString('M8 5L8 19L19 12L8 5', 'SIH 0 1 SFSP 0 1 3'), true], - [ - newPoint(11, 9), - fromPathOpString('M8 5L8 19L19 12L8 5', 'SIH 0 1 S 0 3 1 SFSP 0 1 3'), - true, - ], - ]; - - const TESTS_HIT_TEST_STROKE: [Point, string, number, boolean][] = [ - [newPoint(4, 12), 'M 20 22 L 4 22 L 4 2 L 6 2 L 6 14 L 8 14 L 8 2 L 10 2 L 10 14 Z', 1, true], - [ - newPoint(2, 12), - 'M 20 22 L 4 22 L 4 2 L 6 2 L 6 14 L 8 14 L 8 2 L 10 2 L 10 14 Z', - 1, - false, - ], - [ - newPoint(6, 16), - 'M 20 22 L 4 22 L 4 2 L 6 2 L 6 14 L 8 14 L 8 2 L 10 2 L 10 14 Z', - 1, - false, - ], - ]; - - TESTS_HIT_TEST_FILL.forEach(a => { - const point = a[0] as Point; - const path = typeof a[1] === 'string' ? new Path(a[1] as string) : (a[1] as Path); - it(`hit test for '(${point.x},${point.y})' on fill path '${a[1]}' yields '${a[2]}'`, () => { - expect(path.hitTest(point, { findShapesInRange: true }).isShapeHit).toEqual( - a[2] as boolean, - ); - }); - }); - - TESTS_HIT_TEST_STROKE.forEach(a => { - const point = a[0] as Point; - const path = new Path(a[1] as string); - it(`hit test for '(${point.x},${point.y})' on stroke path '${a[1]}' yields '${a[3]}'`, () => { - const hitResult = path.hitTest(point, { - isSegmentInRangeFn: dist => dist < a[2], - }); - expect(hitResult.isSegmentHit).toEqual(a[3] as boolean); - }); - }); - }); - - describe('#getPointAtLength', () => { - const TESTS: [string, number, Point][] = [['M 0 0 L 0 100', 10, newPoint(0, 10)]]; - - TESTS.forEach(([pathStr, length, expectedPoint]) => { - it(`point at length ${length} on path ${pathStr} yields '(${expectedPoint.x},${ - expectedPoint.y - })`, () => { - expect(new Path(pathStr).getPointAtLength(length)).toEqual(expectedPoint); - }); - }); - }); -}); - -function checkPathsEqual(actual: Path, expected: Path) { - expect(actual.getPathString()).toEqual(expected.getPathString()); - checkCommandsEqual(actual.getCommands(), expected.getCommands()); -} - -function checkCommandsEqual(actual: ReadonlyArray, expected: ReadonlyArray) { - expect(actual.length).toEqual(expected.length); - for (let i = 0; i < actual.length; i++) { - const a = actual[i]; - const e = expected[i]; - expect(a.type).toEqual(e.type); - expect(a.points.length).toEqual(e.points.length); - for (let j = 0; j < a.points.length; j++) { - const ap = a.points[j]; - const ep = e.points[j]; - if (!ap || !ep) { - expect(ap).toEqual(undefined); - expect(ep).toEqual(undefined); - } else { - expect(_.round(ap.x, 8)).toEqual(ep.x); - expect(_.round(ap.y, 8)).toEqual(ep.y); - } - } - } -} - -function newPoint(x: number, y: number) { - return { x, y }; -} diff --git a/src/app/pages/editor/model/paths/Path.ts b/src/app/pages/editor/model/paths/Path.ts deleted file mode 100644 index 462dfe56..00000000 --- a/src/app/pages/editor/model/paths/Path.ts +++ /dev/null @@ -1,1441 +0,0 @@ -import { MathUtil, Matrix, Point } from 'app/pages/editor/scripts/common'; -import { environment } from 'environments/environment'; -import * as _ from 'lodash'; - -import { Projection } from './calculators'; -import { Command } from './Command'; -import { CommandState } from './CommandState'; -import * as PathParser from './PathParser'; -import { PathState } from './PathState'; -import { SubPathState, SubPathStateMutator, flattenSubPathStates } from './SubPathState'; -import { SvgChar } from './SvgChar'; - -/** - * A compound path that contains all of the information associated with a - * PathLayer's pathData attribute. - */ -export class Path { - private readonly ps: PathState; - private pathString: string; - - constructor(obj: string | Command[] | PathState) { - this.ps = typeof obj === 'string' || Array.isArray(obj) ? new PathState(obj) : obj; - if (!environment.production) { - // Don't initialize variables lazily for dev builds (to avoid - // ngrx-store-freeze crashes). - this.getPathString(); - - const allIds = this.getCommands().map(c => c.id); - const uniqueIds = new Set(allIds); - const numCommands = allIds.length; - if (uniqueIds.size !== numCommands) { - const dumpInfo = this.getSubPaths().map((s, subIdx) => { - return s.getCommands().map((c, cmdIdx) => { - return { - subIdx, - cmdIdx, - id: c.id, - isDup: allIds.filter(id => id === c.id).length > 1, - }; - }); - }); - console.warn('duplicate IDs found!', this, _.flatten(dumpInfo)); - } - } - } - - /** - * Returns the path's SVG path string. - */ - getPathString() { - if (this.pathString === undefined) { - this.pathString = PathParser.commandsToString(this.getCommands()); - } - return this.pathString; - } - - /** - * Returns the list of SubPaths in this path. - */ - getSubPaths() { - return this.ps.subPaths; - } - - /** - * Returns the subpath at the specified index. - */ - getSubPath(subIdx: number) { - const numSubPaths = this.getSubPaths().length; - if (subIdx < 0 || numSubPaths <= subIdx) { - console.error(this); - throw new Error( - `Subpath index out of bounds: ` + `subIdx=${subIdx} numSubPaths=${numSubPaths}`, - ); - } - return this.getSubPaths()[subIdx]; - } - - /** - * Returns the list of Commands in this path. - */ - getCommands() { - return this.ps.commands; - } - - /** - * Returns the command at the specified index. - */ - getCommand(subIdx: number, cmdIdx: number) { - const subPath = this.getSubPath(subIdx); - const numCommands = subPath.getCommands().length; - if (cmdIdx < 0 || numCommands <= cmdIdx) { - console.error(this); - throw new Error( - `Command index out of bounds: ` + - `subIdx=${subIdx} cmdIdx=${cmdIdx}, numCommands=${numCommands}`, - ); - } - return subPath.getCommands()[cmdIdx]; - } - - /** - * Returns the length of the path. - */ - getPathLength() { - return this.ps.getPathLength(); - } - - /** - * Returns the length of the subpath. - */ - getSubPathLength(subIdx: number) { - return this.ps.getSubPathLength(subIdx); - } - - /** - * Returns the point at the given length along the path. - */ - getPointAtLength(distance: number) { - return this.ps.getPointAtLength(distance); - } - - /** - * Returns true iff this path is morphable with the specified path. - */ - isMorphableWith(path: Path) { - const cmds1 = this.getCommands(); - const cmds2 = path.getCommands(); - return cmds1.length === cmds2.length && cmds1.every((cmd1, i) => cmd1.type === cmds2[i].type); - } - - /** - * Calculates the point on this path that is closest to the specified point argument. - * Returns undefined if no point is found. - */ - project(point: Point, restrictToSubIdx?: number): ProjectionOntoPath | undefined { - return this.ps.project(point, restrictToSubIdx); - } - - /** - * Performs a hit test on the path and returns a HitResult. - */ - hitTest(point: Point, opts: HitOptions): HitResult { - return this.ps.hitTest(point, opts); - } - - /** - * Returns the pole of inaccessibility for the specified subpath index. - */ - getPoleOfInaccessibility(subIdx: number) { - return this.ps.getPoleOfInaccessibility(subIdx); - } - - /** - * Returns the bounding box for this path. - */ - getBoundingBox() { - return this.ps.getBoundingBox(); - } - - /** - * Returns true iff the subpath at the specified index is clockwise. - */ - isClockwise(subIdx: number) { - return this.ps.isClockwise(subIdx); - } - - /** - * Returns true iff the path is closed. - */ - isClosed() { - return this.getSubPaths().every(s => s.isClosed()); - } - - /** - * Transforms the path using the specified transform matrix. - */ - transform(transform: Matrix) { - return this.mutate() - .transform(transform) - .build() - .clone(); - } - - /** - * Creates a builder that can create a mutated Path object. - */ - mutate() { - return new PathMutator(this.ps); - } - - /** - * Returns a cloned instance of this path. Any existing path state will be cleared. - */ - clone() { - return new Path(this.getPathString()); - } - - /** - * Returns a Path representing its initial unmutated state. - */ - revert() { - return this.mutate() - .revert() - .build(); - } -} - -/** Represents the options for a hit test. */ -export interface HitOptions { - readonly isPointInRangeFn?: (distance: number, cmd?: Command) => boolean; - readonly isSegmentInRangeFn?: (distance: number, cmd?: Command) => boolean; - readonly findShapesInRange?: boolean; - readonly restrictToSubIdx?: ReadonlyArray; -} - -/** Represents the result of a hit test. */ -export interface HitResult { - readonly isHit: boolean; - readonly isEndPointHit: boolean; - readonly isSegmentHit: boolean; - readonly isShapeHit: boolean; - readonly endPointHits?: ReadonlyArray; - readonly segmentHits?: ReadonlyArray; - readonly shapeHits?: Array<{ subIdx: number }>; -} - -export interface ProjectionOntoPath { - readonly subIdx: number; - readonly cmdIdx: number; - readonly projection: Projection; -} - -const ENABLE_LOGS = !environment.production && false; - -/** - * A builder class for creating mutated Path objects. - */ -export class PathMutator { - // A tree of sub path state objects, including collapsing sub paths. - private subPathStateMap: SubPathState[]; - // Maps subIdx --> spsIdx. - private subPathOrdering: number[]; - private numCollapsingSubPaths: number; - - constructor(ps: PathState) { - this.subPathStateMap = [...ps.subPathStateMap]; - this.subPathOrdering = [...ps.subPathOrdering]; - this.numCollapsingSubPaths = ps.numCollapsingSubPaths; - } - - /** - * Reverses the order of the points in the sub path at the specified index. - */ - reverseSubPath(subIdx: number) { - LOG('reverseSubPath', subIdx); - this.setSubPathStateLeaf( - subIdx, - this.findSubPathStateLeaf(subIdx) - .mutate() - .reverse() - .build(), - ); - return this; - } - - /** - * Shifts back the order of the points in the sub path at the specified index. - */ - shiftSubPathBack(subIdx: number, numShifts = 1) { - LOG('shiftSubPathBack', subIdx, numShifts); - return this.findSubPathStateLeaf(subIdx).isReversed() - ? this.shift(subIdx, (o, n) => (o + numShifts) % (n - 1)) - : this.shift(subIdx, (o, n) => MathUtil.floorMod(o - numShifts, n - 1)); - } - - /** - * Shifts forward the order of the points in the sub path at the specified index. - */ - shiftSubPathForward(subIdx: number, numShifts = 1) { - LOG('shiftSubPathForward', subIdx, numShifts); - return this.findSubPathStateLeaf(subIdx).isReversed() - ? this.shift(subIdx, (o, n) => MathUtil.floorMod(o - numShifts, n - 1)) - : this.shift(subIdx, (o, n) => (o + numShifts) % (n - 1)); - } - - private shift(subIdx: number, calcOffsetFn: (offset: number, numCommands: number) => number) { - const sps = this.findSubPathStateLeaf(subIdx); - const numCmdsInSubPath = _.sumBy(sps.getCommandStates(), cs => cs.getCommands().length); - if (numCmdsInSubPath <= 1) { - return this; - } - const firstCmd = sps.getCommandStates()[0].getCommands()[0]; - const lastCmd = _.last(_.last(sps.getCommandStates()).getCommands()); - if (!MathUtil.arePointsEqual(firstCmd.end, lastCmd.end)) { - // TODO: in some cases there may be rounding errors that cause a closed subpath - // to show up as non-closed. is there anything we can do to alleviate this? - console.warn('Ignoring attempt to shift a non-closed subpath'); - return this; - } - this.setSubPathStateLeaf( - subIdx, - sps - .mutate() - .setShiftOffset(calcOffsetFn(sps.getShiftOffset(), numCmdsInSubPath)) - .build(), - ); - return this; - } - - /** - * Splits the command using the specified t values. - */ - splitCommand(subIdx: number, cmdIdx: number, ...ts: number[]) { - LOG('splitCommand', subIdx, cmdIdx, ts); - if (!ts.length) { - throw new Error('Must specify at least one t value'); - } - const { targetCs, csIdx, splitIdx } = this.findReversedAndShiftedInternalIndices( - subIdx, - cmdIdx, - ); - const shiftOffset = this.getUpdatedShiftOffsetsAfterSplit(subIdx, csIdx, splitIdx, ts.length); - const sps = this.findSubPathStateLeaf(subIdx); - if (sps.isReversed()) { - ts = ts.map(t => 1 - t); - } - this.setSubPathStateLeaf( - subIdx, - this.findSubPathStateLeaf(subIdx) - .mutate() - .setShiftOffset(shiftOffset) - .setCommandState( - csIdx, - targetCs - .mutate() - .splitAtIndex(splitIdx, ts) - .build(), - ) - .build(), - ); - return this; - } - - /** - * Splits the command into two approximately equal parts. - */ - splitCommandInHalf(subIdx: number, cmdIdx: number) { - LOG('splitCommandInHalf', subIdx, cmdIdx); - const { targetCs, csIdx, splitIdx } = this.findReversedAndShiftedInternalIndices( - subIdx, - cmdIdx, - ); - const shiftOffset = this.getUpdatedShiftOffsetsAfterSplit(subIdx, csIdx, splitIdx, 1); - this.setSubPathStateLeaf( - subIdx, - this.findSubPathStateLeaf(subIdx) - .mutate() - .setShiftOffset(shiftOffset) - .setCommandState( - csIdx, - targetCs - .mutate() - .splitInHalfAtIndex(splitIdx) - .build(), - ) - .build(), - ); - return this; - } - - // If 0 <= position <= shiftOffset, then that means we need to increase the - // shift offset to account for the new split points that are about to be inserted. - // Note that this method assumes all splits will occur within the same cmdIdx - // command. This means that the shift offset will only ever increase by either - // 'numShifts' or '0', since it will be impossible for splits to be added on - // both sides of the shift pivot. We could fix that, but it's a lot of - // complicated indexing and I don't think the user will ever need to do this anyway. - private getUpdatedShiftOffsetsAfterSplit( - subIdx: number, - csIdx: number, - splitIdx: number, - numSplits: number, - ) { - const sps = this.findSubPathStateLeaf(subIdx); - const shiftOffset = sps.getShiftOffset(); - let position = splitIdx; - for (let i = 0; i < csIdx; i++) { - position += sps.getCommandStates()[i].getCommands().length; - } - if (shiftOffset && position <= shiftOffset) { - return shiftOffset + numSplits; - } - return shiftOffset; - } - - /** - * Un-splits the path at the specified index. Returns a new path object. - */ - unsplitCommand(subIdx: number, cmdIdx: number) { - LOG('unsplitCommand', subIdx, cmdIdx); - const { targetCs, csIdx, splitIdx } = this.findReversedAndShiftedInternalIndices( - subIdx, - cmdIdx, - ); - const isSubPathReversed = this.findSubPathStateLeaf(subIdx).isReversed(); - this.setSubPathStateLeaf( - subIdx, - this.findSubPathStateLeaf(subIdx) - .mutate() - .setCommandState( - csIdx, - targetCs - .mutate() - .unsplitAtIndex(isSubPathReversed ? splitIdx - 1 : splitIdx) - .build(), - ) - .build(), - ); - const sps = this.findSubPathStateLeaf(subIdx); - const shiftOffset = sps.getShiftOffset(); - let position = splitIdx; - for (let i = 0; i < csIdx; i++) { - position += sps.getCommandStates()[i].getCommands().length; - } - if (shiftOffset && position <= shiftOffset) { - // Subtract the shift offset by 1 to ensure that the unsplit operation - // doesn't alter the positions of the path points. - this.setSubPathStateLeaf( - subIdx, - this.findSubPathStateLeaf(subIdx) - .mutate() - .setShiftOffset(shiftOffset - 1) - .build(), - ); - } - return this; - } - - /** - * Convert the path at the specified index. Returns a new path object. - */ - convertCommand(subIdx: number, cmdIdx: number, svgChar: SvgChar) { - const { targetCs, csIdx, splitIdx } = this.findReversedAndShiftedInternalIndices( - subIdx, - cmdIdx, - ); - this.setSubPathStateLeaf( - subIdx, - this.findSubPathStateLeaf(subIdx) - .mutate() - .setCommandState( - csIdx, - targetCs - .mutate() - .convertAtIndex(splitIdx, svgChar) - .build(), - ) - .build(), - ); - return this; - } - - /** - * Reverts any conversions previously performed in the specified sub path. - */ - unconvertSubPath(subIdx: number) { - const sps = this.findSubPathStateLeaf(subIdx); - const css = sps.getCommandStates().map((cs, csIdx) => { - return csIdx === 0 - ? cs - : cs - .mutate() - .unconvertSubpath() - .build(); - }); - this.setSubPathStateLeaf( - subIdx, - sps - .mutate() - .setCommandStates(css) - .build(), - ); - return this; - } - - /** - * Transforms the path using the specified transformation matrix. - */ - transform(transform: Matrix) { - const spss = flattenSubPathStates(this.subPathStateMap); - for (let spsIdx = 0; spsIdx < spss.length; spsIdx++) { - const sps = spss[spsIdx]; - const css = sps.getCommandStates(); - const subIdx = this.subPathOrdering.indexOf(spsIdx); - this.setSubPathStateLeaf( - subIdx, - sps - .mutate() - .setCommandStates( - css.map(cs => - cs - .mutate() - .transform(transform) - .build(), - ), - ) - .build(), - ); - } - return this; - } - - /** - * Moves a subpath from one index to another. Returns a new path object. - */ - moveSubPath(fromSubIdx: number, toSubIdx: number) { - LOG('moveSubPath', fromSubIdx, toSubIdx); - this.subPathOrdering.splice(toSubIdx, 0, this.subPathOrdering.splice(fromSubIdx, 1)[0]); - return this; - } - - /** - * Splits a stroked sub path using the specified indices. - * A 'moveTo' command will be inserted after the command at 'cmdIdx'. - */ - splitStrokedSubPath(subIdx: number, cmdIdx: number) { - LOG('splitStrokedSubPath', subIdx, cmdIdx); - const sps = this.findSubPathStateLeaf(subIdx); - const css = reverseAndShiftCommandStates( - sps.getCommandStates(), - sps.isReversed(), - sps.getShiftOffset(), - ); - const { csIdx, splitIdx } = this.findInternalIndices(css, cmdIdx); - const startCommandStates: CommandState[] = []; - const endCommandStates: CommandState[] = []; - for (let i = 0; i < css.length; i++) { - if (i < csIdx) { - startCommandStates.push(css[i]); - } else if (csIdx < i) { - endCommandStates.push(css[i]); - } else { - const splitPoint = css[i].getCommands()[splitIdx].end; - const { left, right } = css[i].slice(splitIdx); - startCommandStates.push(left); - let endMoveCs = new CommandState(new Command('M', [splitPoint, splitPoint])); - if (sps.isReversed()) { - endMoveCs = endMoveCs - .mutate() - .reverse() - .build(); - } - endCommandStates.push(endMoveCs); - if (right) { - endCommandStates.push(right); - } - } - } - const splitSubPaths = [ - new SubPathState(startCommandStates), - new SubPathState(endCommandStates), - ]; - this.setSubPathStateLeaf( - subIdx, - sps - .mutate() - .setSplitSubPaths(splitSubPaths) - .build(), - ); - this.subPathOrdering.push(this.subPathOrdering.length); - return this; - } - - /** - * Deletes the stroked subpath at the specified index. The subpath's sibling - * will be deleted as well. - */ - deleteStrokedSubPath(subIdx: number) { - LOG('unsplitStrokedSubPath', subIdx); - const parent = this.findSubPathStateParent(subIdx); - const splitId = _.last(_.last(parent.getSplitSubPaths()[0].getCommandStates()).getCommands()) - .id; - const mutator = parent.mutate().setSplitSubPaths([]); - this.deleteSpsSplitPoint(parent.getCommandStates(), splitId, mutator); - this.subPathStateMap = this.replaceSubPathStateNode( - parent, - (states, i) => (states[i] = mutator.build()), - ); - this.updateOrderingAfterUnsplitSubPath(subIdx); - return this; - } - - /** - * Splits a filled subpath using the specified indices. - * - * Consider the following filled subpath: - * - * 2-------------------3 - * | | - * | | - * | | - * 1 4 - * | | - * | | - * | | - * 0-------------------5 - * - * Splitting the filled subpath with startCmdIdx=1 and endCmdIdx=4 - * results in the following split subpaths: - * - * xxxxxxxxxxxxxxxxxxxxx 1-------->>>--------2 - * x x | | - * x x ↑ ↓ - * x x | | - * 1-------->>>--------2 0--------<<<--------3 - * | | x x - * ↑ ↓ x x - * | | x x - * 0--------<<<--------3 xxxxxxxxxxxxxxxxxxxxx - */ - splitFilledSubPath(subIdx: number, startCmdIdx: number, endCmdIdx: number) { - LOG('splitFilledSubPath', subIdx, startCmdIdx, endCmdIdx); - const targetSps = this.findSubPathStateLeaf(subIdx); - const targetCss = reverseAndShiftCommandStates( - targetSps.getCommandStates(), - targetSps.isReversed(), - targetSps.getShiftOffset(), - ); - - const findTargetSplitIdxs = () => { - let s = this.findInternalIndices(targetCss, startCmdIdx); - let e = this.findInternalIndices(targetCss, endCmdIdx); - if (s.csIdx > e.csIdx || (s.csIdx === e.csIdx && s.splitIdx > e.csIdx)) { - // Make sure the start index appears before the end index in the path. - const temp = s; - s = e; - e = temp; - } - return { - startCsIdx: s.csIdx, - startSplitIdx: s.splitIdx, - endCsIdx: e.csIdx, - endSplitIdx: e.splitIdx, - }; - }; - - // firstLeft: left portion of the 1st split segment (used in the 1st split path). - // secondLeft: left portion of the 2nd split segment (used in the 2nd split path). - // firstRight: right portion of the 1st split segment (used in the 2nd split path). - // secondRight: right portion of the 2nd split segment (used in the 1st split path). - const { startCsIdx, startSplitIdx, endCsIdx, endSplitIdx } = findTargetSplitIdxs(); - const { left: firstLeft, right: firstRight } = targetCss[startCsIdx].slice(startSplitIdx); - const { left: secondLeft, right: secondRight } = targetCss[endCsIdx].slice(endSplitIdx); - const startSplitCmd = firstLeft.getCommands()[startSplitIdx]; - const startSplitPoint = startSplitCmd.end; - const endSplitCmd = secondLeft.getCommands()[endSplitIdx]; - const endSplitPoint = endSplitCmd.end; - - // Give both line segments the same unique ID so that we can later identify which - // split segments were added together during the deletion phase. - const splitSegmentId = _.uniqueId(); - const endLine = new CommandState(new Command('L', [endSplitPoint, startSplitPoint])) - .mutate() - .setSplitSegmentInfo(secondLeft, splitSegmentId) - .build(); - const startLine = new CommandState(new Command('L', [startSplitPoint, endSplitPoint])) - .mutate() - .setSplitSegmentInfo(firstLeft, splitSegmentId) - .build(); - - const startCommandStates: CommandState[] = []; - for (let i = 0; i < targetCss.length; i++) { - if (i < startCsIdx || endCsIdx < i) { - startCommandStates.push(targetCss[i]); - } else if (i === startCsIdx) { - startCommandStates.push(firstLeft); - startCommandStates.push(startLine); - } else if (i === endCsIdx && secondRight) { - startCommandStates.push(secondRight); - } - } - - const endCommandStates: CommandState[] = []; - for (let i = 0; i < targetCss.length; i++) { - if (i === startCsIdx) { - endCommandStates.push( - new CommandState(new Command('M', [startSplitPoint, startSplitPoint])) - .mutate() - // The move command identifies the beginning of a new split segment, - // so we'll mark it with the parent state as well (we'll need this - // information later on if the segment is deleted). Note that unlike - // the two segments above, we don't need to specify an ID here. - .setSplitSegmentInfo(firstLeft, '') - .build(), - ); - if (firstRight) { - endCommandStates.push(firstRight); - } - } else if (startCsIdx < i && i < endCsIdx) { - endCommandStates.push(targetCss[i]); - } else if (i === endCsIdx) { - endCommandStates.push(secondLeft); - endCommandStates.push(endLine); - } - } - - const splitSubPaths = [ - new SubPathState(startCommandStates), - new SubPathState(endCommandStates), - ]; - const newStates: SubPathState[] = []; - const parent = this.findSubPathStateParent(subIdx); - - // Find the backing IDs for each parent command state that is a split segment. - const parentSplitBackingIds = _((parent ? parent.getCommandStates() : []) as CommandState[]) - .filter(cs => !!cs.getSplitSegmentId()) - .map(cs => cs.getBackingId()) - .value(); - - // Find the backing IDs for each sibling command state that is a split segment, - // not including split segments that were inherited from the parent. - const siblingSplitBackingIds = _(targetSps.getCommandStates() as CommandState[]) - .filter(cs => !!cs.getSplitSegmentId() && !parentSplitBackingIds.includes(cs.getBackingId())) - .map(cs => cs.getBackingId()) - .value(); - - // Checking for the existence of 'firstRight' and 'secondRight' ensures that - // paths connected to the end point of a deleted split segment will still be kept. - if ( - this.subPathStateMap.includes(targetSps) || - (firstRight && siblingSplitBackingIds.includes(firstLeft.getBackingId())) || - (secondRight && siblingSplitBackingIds.includes(secondLeft.getBackingId())) - ) { - // If we are at the first level of the tree or if one of the new - // split edges is a split segment, then add a new level to the tree. - // If the already existing split segment is deleted, we want to - // delete the split segment we are creating right now as well. - newStates.push( - targetSps - .mutate() - .setSplitSubPaths(splitSubPaths) - .build(), - ); - } else { - // Otherwise insert the sub paths in the current level of the tree. - newStates.push(...splitSubPaths); - } - - // Insert the new SubPathStates into the tree. - this.subPathStateMap = this.replaceSubPathStateNode(targetSps, (states, i) => - states.splice(i, 1, ...newStates), - ); - this.subPathOrdering.push(this.subPathOrdering.length); - return this; - } - - /** - * Deletes the filled subpath at the specified index. All adjacent sibling subpaths - * will be deleted as well (i.e. subpaths that share the same split segment ID). - */ - deleteFilledSubPath(subIdx: number) { - LOG('deleteFilledSubPath', subIdx); - const targetCss = this.findSubPathStateLeaf(subIdx).getCommandStates(); - // Get the list of parent split segment IDs. - const parentSplitSegIds = _(this.findSubPathStateParent( - subIdx, - ).getCommandStates() as CommandState[]) - .map(cs => cs.getSplitSegmentId()) - .compact() - .uniq() - .value(); - // Get the list of sibling split segment IDs, not including split segment - // IDs inherited from the parent. - const siblingSplitSegIds = _(targetCss as CommandState[]) - .map(cs => cs.getSplitSegmentId()) - .compact() - .uniq() - .difference(parentSplitSegIds) - .value(); - siblingSplitSegIds.forEach(id => { - const targetCs = _.find(targetCss, cs => cs.getSplitSegmentId() === id); - const deletedSubIdxs = this.calculateDeletedSubIdxs(subIdx, targetCs); - this.deleteFilledSubPathSegmentInternal(subIdx, targetCs); - subIdx -= _.sumBy(deletedSubIdxs, idx => (idx <= subIdx ? 1 : 0)); - }); - return this; - } - - /** - * Deletes the subpath split segment with the specified indices. - */ - deleteFilledSubPathSegment(subIdx: number, cmdIdx: number) { - LOG('deleteSubPathSplitSegment', subIdx, cmdIdx); - const { targetCs } = this.findReversedAndShiftedInternalIndices(subIdx, cmdIdx); - this.deleteFilledSubPathSegmentInternal(subIdx, targetCs); - return this; - } - - /** - * Deletes the subpath split segment with the specified index. The two subpaths - * that share the split segment ID will be merged into a single subpath. - */ - private deleteFilledSubPathSegmentInternal(subIdx: number, targetCs: CommandState) { - // Get the SubPathState ID of the node representing the subpath with index 'subIdx'. - const targetSpsId = this.findSubPathStateLeaf(subIdx).getId(); - // Get the split segment ID of the target command state object. - const targetSplitSegId = targetCs.getSplitSegmentId(); - const psps = this.findSplitSegmentParentNode(targetSplitSegId); - const pssps = psps.getSplitSubPaths(); - const pcss = psps.getCommandStates(); - // Find the first index of the split sub path containing the target. - const splitSubPathIdx1 = _.findIndex(pssps, sps => { - return sps.getCommandStates().some(cs => cs.getSplitSegmentId() === targetSplitSegId); - }); - // Find the second index of the split sub path containing the target. - const splitSubPathIdx2 = _.findLastIndex(pssps, sps => { - return sps.getCommandStates().some(cs => cs.getSplitSegmentId() === targetSplitSegId); - }); - const deletedSubIdxs = this.calculateDeletedSubIdxs(subIdx, targetCs); - const splitCss1 = pssps[splitSubPathIdx1].getCommandStates(); - const splitCss2 = pssps[splitSubPathIdx2].getCommandStates(); - let updatedSplitSubPaths: SubPathState[] = []; - if (pssps.length > 2) { - // In addition to deleting the split segment, we will also have to merge its - // two adjacent sub paths together into one. - const parentBackingId2 = _.last(splitCss2) - .getParentCommandState() - .getBackingId(); - const parentBackingCmd2 = _.find(pcss, c => parentBackingId2 === c.getBackingId()); - - const newCss: CommandState[] = []; - let cs: CommandState; - let i = 0; - for (; i < splitCss1.length; i++) { - cs = splitCss1[i]; - if (splitCss1[i + 1].getSplitSegmentId() === targetSplitSegId) { - // Iterate until we reach the location of the first split. - break; - } - newCss.push(cs); - } - const parentBackingCmdIdx1 = i; - if (cs.getBackingId() === splitCss2[1].getBackingId()) { - newCss.push(splitCss2[1].merge(cs)); - } else { - newCss.push(cs); - newCss.push(splitCss2[1]); - } - cs = undefined; - for (i = 2; i < splitCss2.length - 1; i++) { - cs = splitCss2[i]; - if (splitCss2[i + 1].getSplitSegmentId() === targetSplitSegId) { - // Iterate until we reach the location of the second split. - break; - } - newCss.push(cs); - } - i = _.findIndex(splitCss1, c => c.getBackingId() === parentBackingCmd2.getBackingId()); - if (i >= 0) { - if (cs) { - if (splitCss1[i].getBackingId() === cs.getBackingId()) { - // If the split created a new point, then merge the left/right commands - // together to reconstruct the previous state. - newCss.push(splitCss1[i].merge(cs)); - } else if (cs) { - // If the split was done at an existing point, then simply push the next - // command state onto the list. - newCss.push(cs); - } - } - } else { - i = parentBackingCmdIdx1 + 1; - if (cs) { - newCss.push(cs); - } - } - for (i = i + 1; i < splitCss1.length; i++) { - newCss.push(splitCss1[i]); - } - const splits = [...pssps]; - splits[splitSubPathIdx1] = new SubPathState([...newCss]) - .mutate() - .setId(targetSpsId) - .build(); - splits.splice(splitSubPathIdx2, 1); - updatedSplitSubPaths = splits; - } - const mutator = psps.mutate().setSplitSubPaths(updatedSplitSubPaths); - const firstSplitSegId = _.last(splitCss2[0].getParentCommandState().getCommands()).id; - const secondSplitSegId = _.last( - _.last(splitCss2) - .getParentCommandState() - .getCommands(), - ).id; - for (const id of [firstSplitSegId, secondSplitSegId]) { - this.deleteSpsSplitPoint(pcss, id, mutator); - } - this.subPathStateMap = this.replaceSubPathStateNode( - psps, - (states, i) => (states[i] = mutator.build()), - ); - for (const idx of deletedSubIdxs) { - this.updateOrderingAfterUnsplitSubPath(idx); - } - return this; - } - - /** - * Calculates the sub path indices that will be removed after unsplitting subIdx. - * targetCs is the command state object containing the split segment in question. - */ - private calculateDeletedSubIdxs(subIdx: number, targetCs: CommandState) { - const splitSegId = targetCs.getSplitSegmentId(); - const psps = this.findSplitSegmentParentNode(splitSegId); - const pssps = psps.getSplitSubPaths(); - const splitSubPathIdx1 = _.findIndex(pssps, sps => { - return sps.getCommandStates().some(cs => cs.getSplitSegmentId() === splitSegId); - }); - const splitSubPathIdx2 = _.findLastIndex(pssps, sps => { - return sps.getCommandStates().some(cs => cs.getSplitSegmentId() === splitSegId); - }); - const pssp1 = pssps[splitSubPathIdx1]; - const pssp2 = pssps[splitSubPathIdx2]; - const deletedSps = [...flattenSubPathStates([pssp1]), ...flattenSubPathStates([pssp2])]; - const spss = flattenSubPathStates(this.subPathStateMap); - return deletedSps - .slice(1) - .map(sps => this.subPathOrdering[spss.indexOf(sps)]) - .sort((a, b) => b - a); - } - - private deleteSpsSplitPoint( - css: ReadonlyArray, - splitCmdId: string, - mutator: SubPathStateMutator, - ) { - let csIdx = 0, - splitIdx = -1; - for (; csIdx < css.length; csIdx++) { - const cs = css[csIdx]; - const csIds = cs.getCommands().map((unused, idx) => cs.getIdAtIndex(idx)); - splitIdx = csIds.indexOf(splitCmdId); - if (splitIdx >= 0) { - break; - } - } - if (splitIdx >= 0 && css[csIdx].isSplitAtIndex(splitIdx)) { - // Delete the split point that created the sub path. - const unsplitCs = css[csIdx] - .mutate() - .unsplitAtIndex(splitIdx) - .build(); - mutator.setCommandState(csIdx, unsplitCs); - } - } - - private updateOrderingAfterUnsplitSubPath(subIdx: number) { - const spsIdx = this.subPathOrdering[subIdx]; - this.subPathOrdering.splice(subIdx, 1); - // tslint:disable-next-line: prefer-for-of - for (let i = 0; i < this.subPathOrdering.length; i++) { - if (spsIdx < this.subPathOrdering[i]) { - this.subPathOrdering[i]--; - } - } - } - - /** - * Adds a collapsing subpath to the path. - */ - addCollapsingSubPath(point: Point, numCommands: number) { - const prevCmd = _.last(this.buildOrderedCommands()); - const css = [new CommandState(new Command('M', [prevCmd.end, point]))]; - for (let i = 1; i < numCommands; i++) { - css.push(new CommandState(new Command('L', [point, point]))); - } - this.subPathStateMap.push(new SubPathState(css)); - this.subPathOrdering.push(this.subPathOrdering.length); - this.numCollapsingSubPaths++; - return this; - } - - /** - * Deletes all collapsing subpaths from the path. - */ - deleteCollapsingSubPaths() { - const numSubPathsBeforeDelete = this.subPathOrdering.length; - const spsIdxToSubIdxMap: number[] = []; - for (let spsIdx = 0; spsIdx < numSubPathsBeforeDelete; spsIdx++) { - spsIdxToSubIdxMap.push(this.subPathOrdering.indexOf(spsIdx)); - } - const numCollapsingSubPathsBeforeDelete = this.numCollapsingSubPaths; - const numSubPathsAfterDelete = numSubPathsBeforeDelete - numCollapsingSubPathsBeforeDelete; - function deleteCollapsingSubPathInfoFn(arr: T[]) { - arr.splice(numSubPathsAfterDelete, numCollapsingSubPathsBeforeDelete); - } - for (let i = 0; i < numCollapsingSubPathsBeforeDelete; i++) { - this.subPathStateMap.pop(); - } - deleteCollapsingSubPathInfoFn(spsIdxToSubIdxMap); - this.subPathOrdering = []; - for (let subIdx = 0; subIdx < numSubPathsBeforeDelete; subIdx++) { - for (let i = 0; i < spsIdxToSubIdxMap.length; i++) { - if (spsIdxToSubIdxMap[i] === subIdx) { - this.subPathOrdering.push(i); - break; - } - } - } - this.numCollapsingSubPaths = 0; - return this; - } - - /** - * Returns the initial starting state of this path. - */ - revert() { - this.deleteCollapsingSubPaths(); - this.subPathStateMap = this.subPathStateMap.map(sps => sps.revert()); - this.subPathOrdering = this.subPathStateMap.map((unused, i) => i); - return this; - } - - /** - * Builds a new mutated path. - */ - build() { - return new Path( - new PathState( - this.buildOrderedCommands(), - this.subPathStateMap, - this.subPathOrdering, - this.numCollapsingSubPaths, - ), - ); - } - - private buildOrderedCommands() { - const spsCmds = flattenSubPathStates(this.subPathStateMap).map(sps => - reverseAndShiftCommands(sps), - ); - const orderedSubPathCmds = this.subPathOrdering.map( - (unused, subIdx) => spsCmds[this.subPathOrdering[subIdx]], - ); - return _(orderedSubPathCmds) - .map((cmds, subIdx) => { - const moveCmd = cmds[0]; - if (subIdx === 0 && moveCmd.start) { - cmds[0] = moveCmd - .mutate() - .setPoints(undefined, moveCmd.end) - .build(); - } else if (subIdx !== 0) { - const start = _.last(orderedSubPathCmds[subIdx - 1]).end; - cmds[0] = moveCmd - .mutate() - .setPoints(start, moveCmd.end) - .build(); - } - return cmds; - }) - .flatMap(cmds => cmds) - .value(); - } - - /** - * Returns the leaf node at the specified subpath index. - */ - private findSubPathStateLeaf(subIdx: number) { - return flattenSubPathStates(this.subPathStateMap)[this.subPathOrdering[subIdx]]; - } - - /** - * Replaces the leaf node at the specified subpath index. - */ - private setSubPathStateLeaf(subIdx: number, newState: SubPathState) { - this.subPathStateMap = this.replaceSubPathStateNode( - this.findSubPathStateLeaf(subIdx), - (states, i) => (states[i] = newState), - ); - } - - /** - * Returns the immediate parent of the leaf node at the specified subpath index. - */ - private findSubPathStateParent(subIdx: number) { - const subPathStateParents: SubPathState[] = []; - (function recurseFn(currentLevel: ReadonlyArray, parent?: SubPathState) { - currentLevel.forEach(state => { - if (!state.getSplitSubPaths().length) { - subPathStateParents.push(parent); - return; - } - recurseFn(state.getSplitSubPaths(), state); - }); - })(this.subPathStateMap); - return subPathStateParents[this.subPathOrdering[subIdx]]; - } - - /** - * Returns the command state indices associated with the specified command index. - * The targetCs is the command state object that contains the command. The csIdx - * is the index in the sub path state's list of command state objects that points - * to targetCs. The splitIdx is the index into the targetCs pointing to the command. - * This method should only be used during sub path splitting. All other methods should - * use the findReversedAndShiftedInternalIndices() method below. - */ - private findInternalIndices(css: ReadonlyArray, cmdIdx: number) { - let counter = 0; - let csIdx = 0; - for (const targetCs of css) { - if (counter + targetCs.getCommands().length > cmdIdx) { - return { targetCs, csIdx, splitIdx: cmdIdx - counter }; - } - counter += targetCs.getCommands().length; - csIdx++; - } - throw new Error('Error retrieving command mutation'); - } - - /** - * Same as above, except this method first takes reversals and shift offsets - * into account. - */ - private findReversedAndShiftedInternalIndices(subIdx: number, cmdIdx: number) { - const sps = this.findSubPathStateLeaf(subIdx); - const css = sps.getCommandStates(); - const numCommandsInSubPath = _.sumBy(css, cs => cs.getCommands().length); - if (cmdIdx && sps.isReversed()) { - cmdIdx = numCommandsInSubPath - cmdIdx; - } - cmdIdx += sps.getShiftOffset(); - if (cmdIdx >= numCommandsInSubPath) { - // Note that subtracting (numCommandsInSubPath - 1) is intentional here - // (as opposed to subtracting numCommandsInSubPath). - cmdIdx -= numCommandsInSubPath - 1; - } - return this.findInternalIndices(css, cmdIdx); - } - - /** - * Replaces a node in the sub path state map. Note that this function uses - * object equality to determine the location of the node in the tree. - */ - private replaceSubPathStateNode( - nodeToReplace: SubPathState, - replaceNodeFn: (states: SubPathState[], i: number) => void, - ) { - return (function recurseFn(states: SubPathState[]) { - if (!states.length) { - return undefined; - } - for (let i = 0; i < states.length; i++) { - const currentState = states[i]; - if (currentState === nodeToReplace) { - replaceNodeFn(states, i); - return states; - } - const recurseStates = recurseFn([...currentState.getSplitSubPaths()]); - if (recurseStates) { - states[i] = currentState - .mutate() - .setSplitSubPaths(recurseStates) - .build(); - return states; - } - } - // Return undefined to signal that the parent was not found. - return undefined; - })([...this.subPathStateMap]); - } - - /** - * Finds the first node in the tree that contains the specified split segment ID. - */ - private findSplitSegmentParentNode(splitSegId: string): SubPathState { - return (function recurseFn(...states: SubPathState[]): SubPathState { - for (const state of states) { - for (const sps of state.getSplitSubPaths()) { - if (sps.getCommandStates().some(cs => cs.getSplitSegmentId() === splitSegId)) { - return state; - } - const parent = recurseFn(sps); - if (parent) { - return parent; - } - } - } - return undefined; - })(...this.subPathStateMap); - } -} - -/** - * Returns a list of shifted and reversed command state objects. Used during - * subpath splitting when creating new children split subpaths. - */ -function reverseAndShiftCommandStates( - css: ReadonlyArray, - isReversed: boolean, - shiftOffset: number, -) { - // If the last command is a 'Z', replace it with a line before we shift. - // TODO: replacing the 'Z' messes up certain stroke-linejoin values - const newCss = [...css]; - newCss[newCss.length - 1] = _.last(css) - .mutate() - .forceConvertClosepathsToLines() - .build(); - return shiftCommandStates(reverseCommandStates(newCss, isReversed), isReversed, shiftOffset); -} - -/** - * Returns a list of reversed command state objects. - */ -function reverseCommandStates(css: CommandState[], isReversed: boolean) { - if (isReversed) { - const revCss = [ - new CommandState( - new Command('M', [css[0].getCommands()[0].start, _.last(_.last(css).getCommands()).end]), - ), - ]; - for (let i = css.length - 1; i > 0; i--) { - revCss.push( - css[i] - .mutate() - .reverse() - .build(), - ); - } - css = revCss; - } - return css; -} - -/** - * Returns a list of shifted command state objects. - */ -function shiftCommandStates(css: CommandState[], isReversed: boolean, shiftOffset: number) { - if (!shiftOffset || css.length === 1) { - return css; - } - - const numCommands = _.sumBy(css, cs => cs.getCommands().length); - if (isReversed) { - shiftOffset *= -1; - shiftOffset += numCommands - 1; - } - const newCss: CommandState[] = []; - - let counter = 0; - let targetCsIdx: number; - let targetSplitIdx: number; - let targetCs: CommandState; - for (let i = 0; i < css.length; i++) { - const cs = css[i]; - const size = cs.getCommands().length; - if (counter + size <= shiftOffset) { - counter += size; - continue; - } - targetCs = cs; - targetCsIdx = i; - targetSplitIdx = shiftOffset - counter; - break; - } - - newCss.push( - new CommandState( - new Command('M', [css[0].getCommands()[0].start, targetCs.getCommands()[targetSplitIdx].end]), - ), - ); - const { left, right } = targetCs.slice(targetSplitIdx); - if (right) { - newCss.push(right); - } - for (let i = targetCsIdx + 1; i < css.length; i++) { - newCss.push(css[i]); - } - for (let i = 1; i < targetCsIdx; i++) { - newCss.push(css[i]); - } - newCss.push(left); - return newCss; -} - -/** - * Returns a list of reversed and shifted commands. - */ -function reverseAndShiftCommands(subPathState: SubPathState) { - return shiftCommands(subPathState, reverseCommands(subPathState)); -} - -/** - * Returns a list of reversed commands. - */ -function reverseCommands(subPathState: SubPathState) { - const subPathCss = subPathState.getCommandStates(); - const hasOneCmd = subPathCss.length === 1 && subPathCss[0].getCommands().length === 1; - if (hasOneCmd || !subPathState.isReversed()) { - // Nothing to do in these two cases. - return _.flatMap(subPathCss, cm => cm.getCommands() as Command[]); - } - - // Extract the commands from our command mutation map. - const cmds = _.flatMap(subPathCss, cm => { - // Consider a segment A ---- B ---- C with AB split and - // BC non-split. When reversed, we want the user to see - // C ---- B ---- A w/ CB split and BA non-split. - const cmCmds = [...cm.getCommands()]; - if (cmCmds[0].type === 'M') { - return cmCmds; - } - cmCmds[0] = cmCmds[0] - .mutate() - .toggleSplitPoint() - .build(); - cmCmds[cmCmds.length - 1] = cmCmds[cmCmds.length - 1] - .mutate() - .toggleSplitPoint() - .build(); - return cmCmds; - }); - - // If the last command is a 'Z', replace it with a line before we reverse. - // TODO: replacing the 'Z' messes up certain stroke-linejoin values - const lastCmd = _.last(cmds); - if (lastCmd.type === 'Z') { - cmds[cmds.length - 1] = lastCmd - .mutate() - .setSvgChar('L') - .setPoints(...lastCmd.points) - .build(); - } - - // Reverse the commands. - const newCmds: Command[] = []; - for (let i = cmds.length - 1; i > 0; i--) { - newCmds.push( - cmds[i] - .mutate() - .reverse() - .build(), - ); - } - newCmds.unshift( - cmds[0] - .mutate() - .setPoints(cmds[0].start, newCmds[0].start) - .build(), - ); - return newCmds; -} - -/** - * Returns a list of shifted commands. - */ -function shiftCommands(subPathState: SubPathState, cmds: Command[]) { - let shiftOffset = subPathState.getShiftOffset(); - if ( - !shiftOffset || - cmds.length === 1 || - !MathUtil.arePointsEqual(_.first(cmds).end, _.last(cmds).end) - ) { - // If there is no shift offset, the sub path is one command long, - // or if the sub path is not closed, then do nothing. - return cmds; - } - - const numCommands = cmds.length; - if (subPathState.isReversed()) { - shiftOffset *= -1; - shiftOffset += numCommands - 1; - } - - // If the last command is a 'Z', replace it with a line before we shift. - const lastCmd = _.last(cmds); - if (lastCmd.type === 'Z') { - // TODO: replacing the 'Z' messes up certain stroke-linejoin values - cmds[numCommands - 1] = lastCmd - .mutate() - .setSvgChar('L') - .setPoints(...lastCmd.points) - .build(); - } - - const newCmds: Command[] = []; - - // Handle these case separately cause they are annoying and I'm sick of edge cases. - if (shiftOffset === 1) { - newCmds.push( - cmds[0] - .mutate() - .setPoints(cmds[0].start, cmds[1].end) - .build(), - ); - for (let i = 2; i < cmds.length; i++) { - newCmds.push(cmds[i]); - } - newCmds.push(cmds[1]); - return newCmds; - } else if (shiftOffset === numCommands - 1) { - newCmds.push( - cmds[0] - .mutate() - .setPoints(cmds[0].start, cmds[numCommands - 2].end) - .build(), - ); - newCmds.push(_.last(cmds)); - for (let i = 1; i < cmds.length - 1; i++) { - newCmds.push(cmds[i]); - } - return newCmds; - } - - // Shift the sequence of commands. After the shift, the original move - // command will be at index 'numCommands - shiftOffset'. - for (let i = 0; i < numCommands; i++) { - newCmds.push(cmds[(i + shiftOffset) % numCommands]); - } - - // The first start point will either be undefined, - // or the end point of the previous sub path. - const prevMoveCmd = newCmds.splice(numCommands - shiftOffset, 1)[0]; - newCmds.push(newCmds.shift()); - newCmds.unshift( - cmds[0] - .mutate() - .setPoints(prevMoveCmd.start, _.last(newCmds).end) - .build(), - ); - return newCmds; -} - -function LOG(...args: any[]) { - if (ENABLE_LOGS) { - const [obj, ...objs] = args; - // tslint:disable-next-line: no-console - console.info(obj, ...objs); - } -} diff --git a/src/app/pages/editor/model/paths/PathParser.spec.ts b/src/app/pages/editor/model/paths/PathParser.spec.ts deleted file mode 100644 index 81b6169a..00000000 --- a/src/app/pages/editor/model/paths/PathParser.spec.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* tslint:disable:max-line-length */ - -import * as PathParser from './PathParser'; - -class Test { - constructor(readonly before: string, readonly after: string) {} -} - -class Spec { - readonly tests: ReadonlyArray; - constructor(readonly description: string, ...tests: Test[]) { - this.tests = tests; - } -} - -const specs = [ - new Spec(`empty path data`, new Test(``, ``)), - new Spec( - `paths with shorthand lineto commands`, - new Test(`M 0 0 10 10 20 20 30 30`, `M 0 0 L 10 10 L 20 20 L 30 30`), - new Test(`M 0 0 h 10 v 10 h -10 v -10`, `M 0 0 L 10 0 L 10 10 L 0 10 L 0 0`), - ), - new Spec( - `sub paths begin with lowercase 'm'`, - new Test(`m 9 7 -1 1 -8 -8 L 10 10 Z`, `M 9 7 L 8 8 L 0 0 L 10 10 Z`), - new Test(`m -1 1 -15 0 0 -2 15 0 Z`, `M -1 1 L -16 1 L -16 -1 L -1 -1 Z`), - new Test( - `m 9 7 -1 1 -8 -8 L 10 10 Z m -1 1 -15 0 0 -2 15 0 Z`, - `M 9 7 L 8 8 L 0 0 L 10 10 Z M 8 8 L -7 8 L -7 6 L 8 6 Z`, - ), - ), - new Spec( - `sub path begins with lineto command`, - new Test(`l0.0.0.5.0.0.5-0.5.0.0-.5z`, `L 0 0 L 0.5 0 L 0.5 0.5 L 0 0.5 L 0 0 Z`), - ), - new Spec( - `convert arcs to cubic bezier curves`, - new Test( - `M 0 0 A 5 5 0 1 0 10 0`, - `M 0 0 C 0 1.326 0.527 2.598 1.464 3.536 C 2.402 4.473 3.674 5 5 5 C 6.326 5 7.598 4.473 8.536 3.536 C 9.473 2.598 10 1.326 10 0`, - ), - new Test( - `M300,70 a230,230 0 1,0 1,0 z`, - `M 300 70 C 239.067 70.132 180.616 94.474 137.6 137.63 C 94.585 180.787 70.434 239.316 70.5 300.249 C 70.566 361.183 94.844 419.66 137.954 462.722 C 181.064 505.785 239.567 529.999 300.5 529.999 C 361.433 529.999 419.936 505.785 463.046 462.722 C 506.156 419.66 530.434 361.183 530.5 300.249 C 530.566 239.316 506.415 180.787 463.4 137.63 C 420.384 94.474 361.933 70.132 301 70 Z`, - ), - ), - new Spec( - `paths w/ complex arcs and curves`, - new Test( - `M54,9.422c-6.555,6.043-13.558,13.787-17.812,22.27C31.93,23.209,24.926,15.465,18.372,9.422a101.486,101.486,0,0,0,17.811,1.564A101.5,101.5,0,0,0,54,9.422M72.367,0A96.572,96.572,0,0,1,36.183,6.986,96.567,96.567,0,0,1,0,0S36.183,23.482,36.183,46.964C36.183,23.482,72.367,0,72.367,0Z`, - `M 54 9.422 C 47.445 15.465 40.442 23.209 36.188 31.692 C 31.93 23.209 24.926 15.465 18.372 9.422 C 24.251 10.466 30.212 10.99 36.183 10.986 C 42.156 10.99 48.119 10.467 54 9.422 M 72.367 0 C 60.866 4.63 48.581 7.002 36.183 6.986 C 23.786 7.002 11.5 4.63 0 0 C 0 0 36.183 23.482 36.183 46.964 C 36.183 23.482 72.367 0 72.367 0 Z`, - ), - new Test( - `M 10 80 C 38.333 33.333 66.666 33.333 95 80 T 180 80`, - `M 10 80 C 38.333 33.333 66.666 33.333 95 80 Q 95 80 180 80`, - ), - new Test( - `M10 80 Q 52.5 10, 95 80 T 180 80 S 150 150, 180 80`, - `M 10 80 Q 52.5 10 95 80 Q 137.5 150 180 80 C 180 80 150 150 180 80`, - ), - ), - new Spec( - `path w/ scientific notation`, - new Test(`M2.000000,22.000000l20.000000,0.000000 1e0-2e3z`, `M 2 22 L 22 22 L 23 -1978 Z`), - ), - new Spec( - `miscellaneous paths`, - new Test( - `M 1 1 m 2 2, l 3 3 L 3 3 H 4 h4 V5 v5, Q6 6 6 6 q 6 6 6 6t 7 7 T 7 7 C 8 8 8 8 8 8 c 8 8 8 8 8 8 S 9 9 9 9 s 9 9 9 9 A 10 10 0 1 1 10 10 a 10 10 0 1 1 10 10`, - `M 1 1 M 3 3 L 6 6 L 3 3 L 4 3 L 8 3 L 8 5 L 8 10 Q 6 6 6 6 Q 12 12 12 12 Q 12 12 19 19 Q 26 26 7 7 C 8 8 8 8 8 8 C 16 16 16 16 16 16 C 16 16 9 9 9 9 C 9 9 18 18 18 18 C 18.448 20.404 17.998 22.891 16.738 24.987 C 15.477 27.082 13.49 28.644 11.156 29.374 C 8.822 30.105 6.299 29.954 4.069 28.952 C 1.838 27.949 0.051 26.162 -0.952 23.931 C -1.954 21.701 -2.105 19.178 -1.374 16.844 C -0.644 14.51 0.918 12.523 3.013 11.262 C 5.109 10.002 7.596 9.552 10 10 C 10 7.349 11.054 4.804 12.929 2.929 C 14.804 1.054 17.349 0 20 0 C 22.651 0 25.196 1.054 27.071 2.929 C 28.946 4.804 30 7.349 30 10 C 30 12.651 28.946 15.196 27.071 17.071 C 25.196 18.946 22.651 20 20 20`, - ), - new Test( - `M 0.0,-1.0 l 0.0,0.0 c 0.5522847498,0.0 1.0,0.4477152502 1.0,1.0 l 0.0,0.0 c 0.0,0.5522847498 -0.4477152502,1.0 -1.0,1.0 l 0.0,0.0 c -0.5522847498,0.0 -1.0,-0.4477152502 -1.0,-1.0 l 0.0,0.0 c 0.0,-0.5522847498 0.4477152502,-1.0 1.0,-1.0 Z M 7.0,-9.0 c 0.0,0.0 -14.0,0.0 -14.0,0.0 c -1.1044921875,0.0 -2.0,0.8955078125 -2.0,2.0 c 0.0,0.0 0.0,14.0 0.0,14.0 c 0.0,1.1044921875 0.8955078125,2.0 2.0,2.0 c 0.0,0.0 14.0,0.0 14.0,0.0 c 1.1044921875,0.0 2.0,-0.8955078125 2.0,-2.0 c 0.0,0.0 0.0,-14.0 0.0,-14.0 c 0.0,-1.1044921875 -0.8955078125,-2.0 -2.0,-2.0 c 0.0,0.0 0.0,0.0 0.0,0.0 Z`, - `M 0 -1 L 0 -1 C 0.552 -1 1 -0.552 1 0 L 1 0 C 1 0.552 0.552 1 0 1 L 0 1 C -0.552 1 -1 0.552 -1 0 L -1 0 C -1 -0.552 -0.552 -1 0 -1 Z M 7 -9 C 7 -9 -7 -9 -7 -9 C -8.104 -9 -9 -8.104 -9 -7 C -9 -7 -9 7 -9 7 C -9 8.104 -8.104 9 -7 9 C -7 9 7 9 7 9 C 8.104 9 9 8.104 9 7 C 9 7 9 -7 9 -7 C 9 -8.104 8.104 -9 7 -9 C 7 -9 7 -9 7 -9 Z`, - ), - new Test( - `M5.3,13.2c-0.1,0.0 -0.3,0.0 -0.4,-0.1c-0.3,-0.2 -0.4,-0.7 -0.2,-1.0c1.3,-1.9 2.9,-3.4 4.9,-4.5c4.1,-2.2 9.3,-2.2 13.4,0.0c1.9,1.1 3.6,2.5 4.9,4.4c0.2,0.3 0.1,0.8 -0.2,1.0c-0.3,0.2 -0.8,0.1 -1.0,-0.2c-1.2,-1.7 -2.6,-3.0 -4.3,-4.0c-3.7,-2.0 -8.3,-2.0 -12.0,0.0c-1.7,0.9 -3.2,2.3 -4.3,4.0C5.7,13.1 5.5,13.2 5.3,13.2z`, - `M 5.3 13.2 C 5.2 13.2 5 13.2 4.9 13.1 C 4.6 12.9 4.5 12.4 4.7 12.1 C 6 10.2 7.6 8.7 9.6 7.6 C 13.7 5.4 18.9 5.4 23 7.6 C 24.9 8.7 26.6 10.1 27.9 12 C 28.1 12.3 28 12.8 27.7 13 C 27.4 13.2 26.9 13.1 26.7 12.8 C 25.5 11.1 24.1 9.8 22.4 8.8 C 18.7 6.8 14.1 6.8 10.4 8.8 C 8.7 9.7 7.2 11.1 6.1 12.8 C 5.7 13.1 5.5 13.2 5.3 13.2 Z`, - ), - ), -]; - -describe('PathParser', () => { - for (const { description, tests } of specs) { - it(description, () => { - for (const { before, after } of tests) { - expect(PathParser.commandsToString(PathParser.parseCommands(before))).toEqual(after); - } - }); - } -}); diff --git a/src/app/pages/editor/model/paths/PathParser.ts b/src/app/pages/editor/model/paths/PathParser.ts deleted file mode 100644 index 91cf7590..00000000 --- a/src/app/pages/editor/model/paths/PathParser.ts +++ /dev/null @@ -1,621 +0,0 @@ -import { Point } from 'app/pages/editor/scripts/common'; - -import { Command } from './Command'; - -/** - * Takes an SVG path string (i.e. the text specified in the path's 'd' attribute) and returns - * list of Commands that represent the SVG path's individual sequence of instructions. - * Arcs are converted to bezier curves because they make life too complicated. :D - */ -export function parseCommands(pathData: string) { - if (!pathData) { - pathData = ''; - } - pathData = pathData.trim(); - let start = 0; - let end = 1; - - const nodes: Array<{ readonly type: string; readonly params: number[] }> = []; - while (end < pathData.length) { - end = nextStart(pathData, end); - const s = pathData.substring(start, end).trim(); - if (s.length > 0) { - const val = getFloats(s); - nodes.push({ type: s.charAt(0), params: val }); - } - start = end; - end++; - } - if (end - start === 1 && start < pathData.length) { - nodes.push({ type: pathData.charAt(start), params: [] }); - } - const current: [number, number, number, number, number, number] = [0, 0, 0, 0, 0, 0]; - let previousCommand = 'm'; - const builder = new CommandsBuilder(); - for (const n of nodes) { - addCommand(builder, current, previousCommand, n.type, n.params); - previousCommand = n.type; - } - return builder.toCommands(); -} - -function nextStart(s: string, end: number) { - while (end < s.length) { - const c = s.charAt(end); - // Note that 'e' or 'E' are not valid path commands, but could be used for floating - // point numbers' scientific notation. Therefore, when searching for the next command, - // we should ignore 'e' and 'E'. - if ((('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z')) && c !== 'E' && c !== 'e') { - return end; - } - end++; - } - return end; -} - -class ExtractFloatResult { - mEndPosition = 0; - mEndWithNegOrDot = false; -} - -function getFloats(s: string) { - if (s.charAt(0) === 'z' || s.charAt(0) === 'Z') { - return []; - } - const results: number[] = []; - let startPosition = 1; - let endPosition: number; - const result = new ExtractFloatResult(); - const totalLength = s.length; - while (startPosition < totalLength) { - extract(s, startPosition, result); - endPosition = result.mEndPosition; - if (startPosition < endPosition) { - results.push(parseFloat(s.substring(startPosition, endPosition))); - } - if (result.mEndWithNegOrDot) { - startPosition = endPosition; - } else { - startPosition = endPosition + 1; - } - } - return results; -} - -/** - * Calculate the position of the next comma or space or negative sign - * - * @param {string} s the string to search - * @param {number} start the position to start searching - * @param {ExtractFloatResult} result the result of the extraction, including the position of the the starting position - * of next number, whether it is ending with a '-'. - */ -function extract(s: string, start: number, result: ExtractFloatResult) { - let currentIndex = start; - let foundSeparator = false; - result.mEndWithNegOrDot = false; - let secondDot = false; - let isExponential = false; - for (; currentIndex < s.length; currentIndex++) { - const isPrevExponential = isExponential; - isExponential = false; - const currentChar = s.charAt(currentIndex); - switch (currentChar) { - case ' ': - case ',': - foundSeparator = true; - break; - case '-': - if (currentIndex !== start && !isPrevExponential) { - foundSeparator = true; - result.mEndWithNegOrDot = true; - } - break; - case '.': - if (!secondDot) { - secondDot = true; - } else { - foundSeparator = true; - result.mEndWithNegOrDot = true; - } - break; - case 'e': - case 'E': - isExponential = true; - break; - } - if (foundSeparator) { - break; - } - } - result.mEndPosition = currentIndex; -} - -function addCommand( - path: CommandsBuilder, - current: [number, number, number, number, number, number], - prevCmd: string, - cmd: string, - val: number[], -) { - let increment = 2; - let [ - currentX, - currentY, - ctrlPointX, - ctrlPointY, - currentSegmentStartX, - currentSegmentStartY, - ] = current; - let reflectiveCtrlPointX: number; - let reflectiveCtrlPointY: number; - switch (cmd) { - case 'z': - case 'Z': - path.close(); - currentX = currentSegmentStartX; - currentY = currentSegmentStartY; - ctrlPointX = currentSegmentStartX; - ctrlPointY = currentSegmentStartY; - break; - case 'm': - case 'M': - case 'l': - case 'L': - case 't': - case 'T': - increment = 2; - break; - case 'h': - case 'H': - case 'v': - case 'V': - increment = 1; - break; - case 'c': - case 'C': - increment = 6; - break; - case 's': - case 'S': - case 'q': - case 'Q': - increment = 4; - break; - case 'a': - case 'A': - increment = 7; - break; - } - for (let k = 0; k < val.length; k += increment) { - switch (cmd) { - case 'm': - currentX += val[k]; - currentY += val[k + 1]; - if (k > 0) { - path.rLineTo(val[k], val[k + 1]); - } else { - path.rMoveTo(val[k], val[k + 1]); - currentSegmentStartX = currentX; - currentSegmentStartY = currentY; - } - break; - case 'M': - currentX = val[k]; - currentY = val[k + 1]; - if (k > 0) { - path.lineTo(val[k], val[k + 1]); - } else { - path.moveTo(val[k], val[k + 1]); - currentSegmentStartX = currentX; - currentSegmentStartY = currentY; - } - break; - case 'l': - path.rLineTo(val[k], val[k + 1]); - currentX += val[k]; - currentY += val[k + 1]; - break; - case 'L': - path.lineTo(val[k], val[k + 1]); - currentX = val[k]; - currentY = val[k + 1]; - break; - case 'h': - path.rLineTo(val[k], 0); - currentX += val[k]; - break; - case 'H': - path.lineTo(val[k], currentY); - currentX = val[k]; - break; - case 'v': - path.rLineTo(0, val[k]); - currentY += val[k]; - break; - case 'V': - path.lineTo(currentX, val[k]); - currentY = val[k]; - break; - case 'c': - path.rCubicTo(val[k], val[k + 1], val[k + 2], val[k + 3], val[k + 4], val[k + 5]); - ctrlPointX = currentX + val[k + 2]; - ctrlPointY = currentY + val[k + 3]; - currentX += val[k + 4]; - currentY += val[k + 5]; - break; - case 'C': - path.cubicTo(val[k], val[k + 1], val[k + 2], val[k + 3], val[k + 4], val[k + 5]); - currentX = val[k + 4]; - currentY = val[k + 5]; - ctrlPointX = val[k + 2]; - ctrlPointY = val[k + 3]; - break; - case 's': - reflectiveCtrlPointX = 0; - reflectiveCtrlPointY = 0; - if (prevCmd === 'c' || prevCmd === 's' || prevCmd === 'C' || prevCmd === 'S') { - reflectiveCtrlPointX = currentX - ctrlPointX; - reflectiveCtrlPointY = currentY - ctrlPointY; - } - path.rCubicTo( - reflectiveCtrlPointX, - reflectiveCtrlPointY, - val[k], - val[k + 1], - val[k + 2], - val[k + 3], - ); - ctrlPointX = currentX + val[k]; - ctrlPointY = currentY + val[k + 1]; - currentX += val[k + 2]; - currentY += val[k + 3]; - break; - case 'S': - reflectiveCtrlPointX = currentX; - reflectiveCtrlPointY = currentY; - if (prevCmd === 'c' || prevCmd === 's' || prevCmd === 'C' || prevCmd === 'S') { - reflectiveCtrlPointX = 2 * currentX - ctrlPointX; - reflectiveCtrlPointY = 2 * currentY - ctrlPointY; - } - path.cubicTo( - reflectiveCtrlPointX, - reflectiveCtrlPointY, - val[k], - val[k + 1], - val[k + 2], - val[k + 3], - ); - ctrlPointX = val[k]; - ctrlPointY = val[k + 1]; - currentX = val[k + 2]; - currentY = val[k + 3]; - break; - case 'q': - path.rQuadTo(val[k], val[k + 1], val[k + 2], val[k + 3]); - ctrlPointX = currentX + val[k]; - ctrlPointY = currentY + val[k + 1]; - currentX += val[k + 2]; - currentY += val[k + 3]; - break; - case 'Q': - path.quadTo(val[k], val[k + 1], val[k + 2], val[k + 3]); - ctrlPointX = val[k]; - ctrlPointY = val[k + 1]; - currentX = val[k + 2]; - currentY = val[k + 3]; - break; - case 't': - reflectiveCtrlPointX = 0; - reflectiveCtrlPointY = 0; - if (prevCmd === 'q' || prevCmd === 't' || prevCmd === 'Q' || prevCmd === 'T') { - reflectiveCtrlPointX = currentX - ctrlPointX; - reflectiveCtrlPointY = currentY - ctrlPointY; - } - path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, val[k], val[k + 1]); - ctrlPointX = currentX + reflectiveCtrlPointX; - ctrlPointY = currentY + reflectiveCtrlPointY; - currentX += val[k]; - currentY += val[k + 1]; - break; - case 'T': - reflectiveCtrlPointX = currentX; - reflectiveCtrlPointY = currentY; - if (prevCmd === 'q' || prevCmd === 't' || prevCmd === 'Q' || prevCmd === 'T') { - reflectiveCtrlPointX = 2 * currentX - ctrlPointX; - reflectiveCtrlPointY = 2 * currentY - ctrlPointY; - } - path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY, val[k], val[k + 1]); - ctrlPointX = reflectiveCtrlPointX; - ctrlPointY = reflectiveCtrlPointY; - currentX = val[k]; - currentY = val[k + 1]; - break; - case 'a': - drawArc( - path, - currentX, - currentY, - val[k + 5] + currentX, - val[k + 6] + currentY, - val[k], - val[k + 1], - val[k + 2], - val[k + 3] !== 0, - val[k + 4] !== 0, - ); - currentX += val[k + 5]; - currentY += val[k + 6]; - ctrlPointX = currentX; - ctrlPointY = currentY; - break; - case 'A': - drawArc( - path, - currentX, - currentY, - val[k + 5], - val[k + 6], - val[k], - val[k + 1], - val[k + 2], - val[k + 3] !== 0, - val[k + 4] !== 0, - ); - currentX = val[k + 5]; - currentY = val[k + 6]; - ctrlPointX = currentX; - ctrlPointY = currentY; - break; - } - prevCmd = cmd; - } - current[0] = currentX; - current[1] = currentY; - current[2] = ctrlPointX; - current[3] = ctrlPointY; - current[4] = currentSegmentStartX; - current[5] = currentSegmentStartY; -} - -function drawArc( - p: CommandsBuilder, - x0: number, - y0: number, - x1: number, - y1: number, - a: number, - b: number, - theta: number, - isMoreThanHalf: boolean, - isPositiveArc: boolean, -) { - const thetaD = (theta * Math.PI) / 180; - const cosTheta = Math.cos(thetaD); - const sinTheta = Math.sin(thetaD); - const x0p = (x0 * cosTheta + y0 * sinTheta) / a; - const y0p = (-x0 * sinTheta + y0 * cosTheta) / b; - const x1p = (x1 * cosTheta + y1 * sinTheta) / a; - const y1p = (-x1 * sinTheta + y1 * cosTheta) / b; - const dx = x0p - x1p; - const dy = y0p - y1p; - const xm = (x0p + x1p) / 2; - const ym = (y0p + y1p) / 2; - const dsq = dx * dx + dy * dy; - if (dsq === 0.0) { - return; - } - const disc = 1.0 / dsq - 1.0 / 4.0; - if (disc < 0.0) { - const adjust = Math.sqrt(dsq) / 1.99999; - drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta, isMoreThanHalf, isPositiveArc); - return; - } - const s = Math.sqrt(disc); - const sdx = s * dx; - const sdy = s * dy; - let cx: number; - let cy: number; - if (isMoreThanHalf === isPositiveArc) { - cx = xm - sdy; - cy = ym + sdx; - } else { - cx = xm + sdy; - cy = ym - sdx; - } - const eta0 = Math.atan2(y0p - cy, x0p - cx); - const eta1 = Math.atan2(y1p - cy, x1p - cx); - let sweep = eta1 - eta0; - if (isPositiveArc !== sweep >= 0) { - if (sweep > 0) { - sweep -= 2 * Math.PI; - } else { - sweep += 2 * Math.PI; - } - } - cx *= a; - cy *= b; - const tcx = cx; - cx = cx * cosTheta - cy * sinTheta; - cy = tcx * sinTheta + cy * cosTheta; - arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep); -} - -/** - * Converts an arc to cubic Bezier segments and records them in p. - * - * @param {CommandsBuilder} p The target for the cubic Bezier segments - * @param {number} cx The x coordinate center of the ellipse - * @param {number} cy The y coordinate center of the ellipse - * @param {number} a The radius of the ellipse in the horizontal direction - * @param {number} b The radius of the ellipse in the vertical direction - * @param {number} e1x E(eta1) x coordinate of the starting point of the arc - * @param {number} e1y E(eta2) y coordinate of the starting point of the arc - * @param {number} theta The angle that the ellipse bounding rectangle makes with horizontal plane - * @param {number} start The start angle of the arc on the ellipse - * @param {number} sweep The angle (positive or negative) of the sweep of the arc on the ellipse - */ -function arcToBezier( - p: CommandsBuilder, - cx: number, - cy: number, - a: number, - b: number, - e1x: number, - e1y: number, - theta: number, - start: number, - sweep: number, -) { - const numSegments = Math.trunc(Math.ceil(Math.abs((sweep * 4) / Math.PI))); - let eta1 = start; - const cosTheta = Math.cos(theta); - const sinTheta = Math.sin(theta); - const cosEta1 = Math.cos(eta1); - const sinEta1 = Math.sin(eta1); - let ep1x = -a * cosTheta * sinEta1 - b * sinTheta * cosEta1; - let ep1y = -a * sinTheta * sinEta1 + b * cosTheta * cosEta1; - const anglePerSegment = sweep / numSegments; - for (let i = 0; i < numSegments; i++) { - const eta2 = eta1 + anglePerSegment; - const sinEta2 = Math.sin(eta2); - const cosEta2 = Math.cos(eta2); - const e2x = cx + a * cosTheta * cosEta2 - b * sinTheta * sinEta2; - const e2y = cy + a * sinTheta * cosEta2 + b * cosTheta * sinEta2; - const ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2; - const ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2; - const tanDiff2 = Math.tan((eta2 - eta1) / 2); - const alpha = (Math.sin(eta2 - eta1) * (Math.sqrt(4 + 3 * tanDiff2 * tanDiff2) - 1)) / 3; - const q1x = e1x + alpha * ep1x; - const q1y = e1y + alpha * ep1y; - const q2x = e2x - alpha * ep2x; - const q2y = e2y - alpha * ep2y; - p.cubicTo(q1x, q1y, q2x, q2y, e2x, e2y); - eta1 = eta2; - e1x = e2x; - e1y = e2y; - ep1x = ep2x; - ep1y = ep2y; - } -} - -class CommandsBuilder { - private commands: Command[] = []; - private currentSegmentStartX = 0; - private currentSegmentStartY = 0; - private currentX = 0; - private currentY = 0; - - rMoveTo(ri0: number, ri1: number) { - const i0 = this.currentX + ri0; - const i1 = this.currentY + ri1; - this.moveTo(i0, i1); - } - - moveTo(i0: number, i1: number) { - const start = this.commands.length ? { x: this.currentX, y: this.currentY } : undefined; - const end = { x: i0, y: i1 }; - this.commands.push(newMove(start, end)); - this.currentSegmentStartX = i0; - this.currentSegmentStartY = i1; - this.currentX = i0; - this.currentY = i1; - } - - rLineTo(ri0: number, ri1: number) { - const i0 = this.currentX + ri0; - const i1 = this.currentY + ri1; - this.lineTo(i0, i1); - } - - lineTo(i0: number, i1: number) { - const start = { x: this.currentX, y: this.currentY }; - const end = { x: i0, y: i1 }; - this.commands.push(newLine(start, end)); - this.currentX = i0; - this.currentY = i1; - } - - rQuadTo(ri0: number, ri1: number, ri2: number, ri3: number) { - const i0 = this.currentX + ri0; - const i1 = this.currentY + ri1; - const i2 = this.currentX + ri2; - const i3 = this.currentY + ri3; - this.quadTo(i0, i1, i2, i3); - } - - quadTo(i0: number, i1: number, i2: number, i3: number) { - const start = { x: this.currentX, y: this.currentY }; - const cp = { x: i0, y: i1 }; - const end = { x: i2, y: i3 }; - this.commands.push(newQuadraticCurve(start, cp, end)); - this.currentX = i2; - this.currentY = i3; - } - - rCubicTo(ri0: number, ri1: number, ri2: number, ri3: number, ri4: number, ri5: number) { - const i0 = this.currentX + ri0; - const i1 = this.currentY + ri1; - const i2 = this.currentX + ri2; - const i3 = this.currentY + ri3; - const i4 = this.currentX + ri4; - const i5 = this.currentY + ri5; - this.cubicTo(i0, i1, i2, i3, i4, i5); - } - - cubicTo(i0: number, i1: number, i2: number, i3: number, i4: number, i5: number) { - const start = { x: this.currentX, y: this.currentY }; - const cp1 = { x: i0, y: i1 }; - const cp2 = { x: i2, y: i3 }; - const end = { x: i4, y: i5 }; - this.commands.push(newBezierCurve(start, cp1, cp2, end)); - this.currentX = i4; - this.currentY = i5; - } - - close() { - const start = { x: this.currentX, y: this.currentY }; - const end = { x: this.currentSegmentStartX, y: this.currentSegmentStartY }; - this.commands.push(newClosePath(start, end)); - this.currentX = this.currentSegmentStartX; - this.currentY = this.currentSegmentStartY; - } - - toCommands() { - return this.commands; - } -} - -/** Takes an list of Commands and converts them back into a SVG path string. */ -export function commandsToString(commands: ReadonlyArray) { - const tokens: string[] = []; - commands.forEach(cmd => { - tokens.push(cmd.type); - const isClosePathCommand = cmd.type === 'Z'; - const pointsToNumberListFunc = (...points: { x: number; y: number }[]) => - points.reduce((list, p) => [...list, p.x, p.y], [] as number[]); - const args = pointsToNumberListFunc(...(isClosePathCommand ? [] : cmd.points.slice(1))); - tokens.splice(tokens.length, 0, ...args.map(n => Number(n.toFixed(3)).toString())); - }); - return tokens.join(' '); -} - -function newMove(start: Point, end: Point) { - return new Command('M', [start, end]); -} - -function newLine(start: Point, end: Point) { - return new Command('L', [start, end]); -} - -function newQuadraticCurve(start: Point, cp: Point, end: Point) { - return new Command('Q', [start, cp, end]); -} - -function newBezierCurve(start: Point, cp1: Point, cp2: Point, end: Point) { - return new Command('C', [start, cp1, cp2, end]); -} - -function newClosePath(start: Point, end: Point) { - return new Command('Z', [start, end]); -} diff --git a/src/app/pages/editor/model/paths/PathState.ts b/src/app/pages/editor/model/paths/PathState.ts deleted file mode 100644 index 81818c46..00000000 --- a/src/app/pages/editor/model/paths/PathState.ts +++ /dev/null @@ -1,415 +0,0 @@ -import { MathUtil, Point, Rect } from 'app/pages/editor/scripts/common'; -import * as _ from 'lodash'; -import polylabel from 'polylabel'; - -import { CommandState } from './CommandState'; -import * as PathParser from './PathParser'; -import { createSubPaths } from './SubPath'; -import { SubPathState, findSubPathState } from './SubPathState'; -import { Command, HitOptions, HitResult, Projection, ProjectionOntoPath, SubPath } from '.'; - -/** - * Container class that encapsulates a Path's underlying state. - */ -export class PathState { - readonly subPaths: ReadonlyArray; - readonly commands: ReadonlyArray; - - constructor( - obj: string | ReadonlyArray, - // Maps internal spsIdx indices to SubPathState objects. The last 'numCollapsingSubPaths' - // indices hold references to the collapsing sub paths. - readonly subPathStateMap?: ReadonlyArray, - // Maps client-visible subIdx values to their positions in the subPathStateMap. - readonly subPathOrdering?: ReadonlyArray, - // The number of collapsing subpaths appended to the end of the subPathStateMap. - readonly numCollapsingSubPaths = 0, - ) { - const commands = typeof obj === 'string' ? PathParser.parseCommands(obj) : obj; - const subPaths = createSubPaths(commands); - this.subPathStateMap = - subPathStateMap || - subPaths.map(s => new SubPathState(s.getCommands().map(c => new CommandState(c)))); - this.subPathOrdering = subPathOrdering || subPaths.map((unused, i) => i); - this.subPaths = subPaths.map((subPath, subIdx) => { - const cmds = subPath.getCommands().map((cmd, cmdIdx) => { - const { cs, splitIdx } = this.findCommandStateInfo(subIdx, cmdIdx); - return cmd - .mutate() - .setId(cs.getIdAtIndex(splitIdx)) - .build(); - }); - const spsIdx = this.subPathOrdering[subIdx]; - const isCollapsing = this.subPathOrdering.length - this.numCollapsingSubPaths <= spsIdx; - const sps = this.findSubPathState(subIdx); - const isSplit = isSubPathSplit(this.subPathStateMap, spsIdx); - const isUnsplittable = !this.subPathStateMap.includes(sps); - return subPath - .mutate() - .setId(sps.getId()) - .setCommands(cmds) - .setIsCollapsing(isCollapsing) - .setIsReversed(sps.isReversed()) - .setShiftOffset(sps.getShiftOffset()) - .setIsSplit(isSplit) - .setIsUnsplittable(isUnsplittable) - .build(); - }); - this.commands = _.flatMap(this.subPaths, subPath => subPath.getCommands() as Command[]); - } - - getPathLength() { - let length = 0; - for (let i = 0; i < this.subPathStateMap.length; i++) { - length += this.getSubPathLength(i); - } - return length; - } - - getSubPathLength(subIdx: number) { - const sps = this.findSubPathState(subIdx); - return _.sumBy(sps.getCommandStates(), cs => cs.getPathLength()); - } - - getPointAtLength(distance: number) { - const spss = this.subPathStateMap.map((unused, i) => this.findSubPathState(i)); - let length = 0; - for (const sps of spss) { - for (const cs of sps.getCommandStates()) { - const len = cs.getPathLength(); - if (length <= distance && distance < length + len) { - return cs.getPointAtLength(distance - length); - } - length += len; - } - } - return undefined; - } - - project(point: Point, restrictToSubIdx?: number): ProjectionOntoPath | undefined { - interface ReduceArg { - readonly spsIdx: number; - readonly csIdx: number; - readonly splitIdx: number; - readonly projection: Projection; - } - const minProjectionResultInfo = _(this.subPaths as SubPath[]) - .map((subPath, subIdx) => ({ subPath, subIdx })) - .filter( - ({ subPath, subIdx }) => - !subPath.isCollapsing() && - (restrictToSubIdx === undefined || restrictToSubIdx === subIdx), - ) - .map(obj => { - const { subIdx } = obj; - const sps = this.findSubPathState(subIdx); - return sps.getCommandStates().map((cs, csIdx) => { - const csProjection = cs.project(point); - if (csProjection && sps.isReversed()) { - const t = csProjection.projection.t; - csProjection.projection.t = 1 - t; - } - return { - spsIdx: this.subPathOrdering[subIdx], - csIdx, - splitIdx: csProjection ? csProjection.splitIdx : 0, - projection: csProjection ? csProjection.projection : undefined, - }; - }); - }) - .flatMap(projections => projections) - .filter(obj => !!obj.projection) - // Reverse so that commands drawn with higher z-orders are preferred. - .reverse() - .reduce((prev: ReduceArg, curr: ReduceArg) => { - return prev && prev.projection.d < curr.projection.d ? prev : curr; - }, undefined); - if (!minProjectionResultInfo) { - return undefined; - } - const { spsIdx, splitIdx, projection } = minProjectionResultInfo; - const cmdIdx = this.toCmdIdx(spsIdx, minProjectionResultInfo.csIdx, splitIdx); - return { projection, subIdx: this.subPathOrdering.indexOf(spsIdx), cmdIdx }; - } - - hitTest(point: Point, opts: HitOptions = {}): HitResult { - const endPointHits: ProjectionOntoPath[] = []; - const segmentHits: ProjectionOntoPath[] = []; - const shapeHits: Array<{ readonly subIdx: number }> = []; - const defaultRestrictToSubIdx = this.subPaths.map((unused, i) => i); - const restrictToSubIdxSet = new Set(opts.restrictToSubIdx || defaultRestrictToSubIdx); - - if (opts.isPointInRangeFn) { - endPointHits.push( - ..._(this.subPaths as SubPath[]) - .map((subPath, subIdx) => ({ subPath, subIdx })) - .filter(obj => { - const { subPath, subIdx } = obj; - return !subPath.isCollapsing() && restrictToSubIdxSet.has(subIdx); - }) - .map(obj => { - const { subPath, subIdx } = obj; - return subPath.getCommands().map((cmd, cmdIdx) => { - const { x, y } = cmd.end; - const d = MathUtil.distance(cmd.end, point); - const t = 1; - const projection = { x, y, d, t }; - return { subIdx, cmdIdx, projection, cmd }; - }); - }) - .flatMap(pointInfos => pointInfos) - .filter(pointInfo => opts.isPointInRangeFn(pointInfo.projection.d, pointInfo.cmd)) - .map(pointInfo => { - const { subIdx, cmdIdx, projection } = pointInfo; - return { subIdx, cmdIdx, projection }; - }) - .value(), - ); - } - - if (opts.isSegmentInRangeFn) { - // TODO: also check to see if the hit occurred at a stroke-linejoin vertex - // TODO: take stroke width scaling into account as well? - segmentHits.push( - ..._(this.subPaths as SubPath[]) - .map((subPath, subIdx) => ({ subPath, subIdx })) - .filter(obj => { - const { subPath, subIdx } = obj; - return !subPath.isCollapsing() && restrictToSubIdxSet.has(subIdx); - }) - .flatMap(obj => { - const { subIdx } = obj; - const spsIdx = this.subPathOrdering[subIdx]; - const sps = this.findSubPathState(subIdx); - // We iterate by csIdx here to improve performance (since cmdIdx - // values can be split points). - return _.flatMap(sps.getCommandStates(), (cs, csIdx) => { - const projectionWithSplitIdx = cs.project(point); - if (!projectionWithSplitIdx) { - return [] as ProjectionOntoPath[]; - } - const { projection, splitIdx } = projectionWithSplitIdx; - if (sps.isReversed()) { - projection.t = 1 - projection.t; - } - const cmdIdx = this.toCmdIdx(spsIdx, csIdx, splitIdx); - return [{ subIdx, cmdIdx, projection }]; - }); - }) - .filter(obj => { - const cmd = this.subPaths[obj.subIdx].getCommands()[obj.cmdIdx]; - return opts.isSegmentInRangeFn(obj.projection.d, cmd); - }) - .value(), - ); - } - - if (opts.findShapesInRange) { - shapeHits.push( - ..._(this.subPaths as SubPath[]) - .map((subPath, subIdx) => ({ subPath, subIdx })) - .filter(obj => { - const { subPath, subIdx } = obj; - return subPath.isClosed() && !subPath.isCollapsing() && restrictToSubIdxSet.has(subIdx); - }) - .flatMap(obj => { - const { subIdx } = obj; - const css = this.findSubPathState(subIdx).getCommandStates(); - const bounds = createBoundingBox(css); - if (!containsPoint(bounds, point)) { - // Nothing to see here. Check the next subpath. - return [] as Array<{ readonly subIdx: number }>; - } - // The point is inside the subpath's bounding box, so next, we will - // use the 'even-odd rule' to determine if the filled path has been hit. - // We create a line from the mouse point to a point we know that is not - // inside the path (in this case, we use a coordinate outside the path's - // bounded box). A hit has occured if and only if the number of - // intersections between the line and the path is odd. - const line = { p1: point, p2: { x: bounds.r + 1, y: bounds.b + 1 } }; - const intersectionResults = css.map(cs => cs.intersects(line)); - const numIntersections = _.sumBy(intersectionResults, ts => ts.length); - if (numIntersections % 2 === 0) { - // Nothing to see here. Check the next subpath. - return [] as Array<{ readonly subIdx: number }>; - } - return [{ subIdx }]; - }) - .value(), - ); - } - const isEndPointHit = !!endPointHits.length; - const isSegmentHit = !!segmentHits.length; - const isShapeHit = !!shapeHits.length; - const isHit = isEndPointHit || isSegmentHit || isShapeHit; - return { - isHit, - isEndPointHit, - isSegmentHit, - isShapeHit, - endPointHits, - segmentHits, - shapeHits, - }; - } - - // TODO: move this math stuff into the calculators module - // TODO: approximate bezier curves by splitting them up into line segments - // TODO: write tests for this stuff - getPoleOfInaccessibility(subIdx: number) { - const subPathCmds = this.subPaths[subIdx].getCommands(); - if (subPathCmds.length === 1) { - const { end } = subPathCmds[0]; - return { x: end.x, y: end.y }; - } - const cmds = subPathCmds.slice(1); - const polygon = _.flatMap(cmds, cmd => { - const { x: p1x, y: p1y } = cmd.start; - const { x: p2x, y: p2y } = cmd.end; - return [[p1x, p1y], [p2x, p2y]]; - }); - if (cmds.length && !this.subPaths[subIdx].isClosed()) { - const { x: p1x, y: p1y } = cmds[0].start; - const { x: p2x, y: p2y } = _.last(cmds).end; - polygon.push(...[[p1x, p1y], [p2x, p2y]]); - } - const pole = polylabel([polygon]); - return { x: pole[0], y: pole[1] }; - } - - // TODO: cache this? - getBoundingBox() { - const css = _.flatMap(this.subPathStateMap, sps => sps.getCommandStates() as CommandState[]); - return createBoundingBox(css); - } - - isClockwise(subIdx: number) { - const cmds = this.subPaths[subIdx].getCommands(); - return _.sumBy(cmds, cmd => getArea(cmd)) >= 0; - } - - private findSubPathState(subIdx: number) { - return findSubPathState(this.subPathStateMap, this.subPathOrdering[subIdx]); - } - - private findCommandStateInfo(subIdx: number, cmdIdx: number) { - const sps = this.findSubPathState(subIdx); - const numCommandsInSubPath = _.sumBy(sps.getCommandStates(), cs => cs.getCommands().length); - if (cmdIdx && sps.isReversed()) { - cmdIdx = numCommandsInSubPath - cmdIdx; - } - cmdIdx += sps.getShiftOffset(); - if (cmdIdx >= numCommandsInSubPath) { - // Note that subtracting numCommandsInSubPath is intentional here - // (as opposed to subtracting numCommandsInSubPath - 1). - cmdIdx -= numCommandsInSubPath; - } - let counter = 0; - for (const cs of sps.getCommandStates()) { - if (counter + cs.getCommands().length > cmdIdx) { - return { sps, cs, splitIdx: cmdIdx - counter }; - } - counter += cs.getCommands().length; - } - throw new Error('Error retrieving command mutation'); - } - - private toCmdIdx(spsIdx: number, csIdx: number, splitIdx: number) { - const sps = this.findSubPathState(this.subPathOrdering.indexOf(spsIdx)); - const commandStates = sps.getCommandStates(); - const numCmds = _.sumBy(commandStates, cs => cs.getCommands().length); - let cmdIdx = - splitIdx + _.sum(commandStates.map((cs, i) => (i < csIdx ? cs.getCommands().length : 0))); - let shiftOffset = sps.getShiftOffset(); - if (sps.isReversed()) { - cmdIdx = numCmds - cmdIdx; - shiftOffset *= -1; - shiftOffset += numCmds - 1; - } - if (shiftOffset) { - cmdIdx += numCmds - shiftOffset - 1; - if (cmdIdx >= numCmds) { - cmdIdx = cmdIdx - numCmds + 1; - } - } - return cmdIdx; - } -} - -// TODO: cache this? -function createBoundingBox(css: ReadonlyArray) { - const bounds = { l: Infinity, t: Infinity, r: -Infinity, b: -Infinity }; - - const expandBoundsFn = (x: number, y: number) => { - if (isNaN(x) || isNaN(y)) { - return; - } - bounds.l = Math.min(x, bounds.l); - bounds.t = Math.min(y, bounds.t); - bounds.r = Math.max(x, bounds.r); - bounds.b = Math.max(y, bounds.b); - }; - - const expandBoundsForCommandMutationFn = (cs: CommandState) => { - const bbox = cs.getBoundingBox(); - expandBoundsFn(bbox.x.min, bbox.y.min); - expandBoundsFn(bbox.x.max, bbox.y.min); - expandBoundsFn(bbox.x.min, bbox.y.max); - expandBoundsFn(bbox.x.max, bbox.y.max); - }; - - css.forEach(cs => expandBoundsForCommandMutationFn(cs)); - return bounds; -} - -function isSubPathSplit(map: ReadonlyArray, spsIdx: number) { - return !!findSubPathState(map, spsIdx).getSplitSubPaths().length; -} - -function containsPoint(rect: Rect, p: Point) { - return rect.l <= p.x && p.x < rect.r && rect.t <= p.y && p.y < rect.b; -} - -function getArea(cmd: Command) { - if (cmd.type === 'M') { - return 0; - } - const { x: x0, y: y0 } = cmd.start; - const { x: x3, y: y3 } = cmd.end; - let area = 0; - switch (cmd.type) { - case 'L': - case 'Z': - area = (x3 - x0) * (y3 - y0); - break; - case 'Q': - case 'C': - let x1: number; - let y1: number; - let x2: number; - let y2: number; - if (cmd.type === 'Q') { - const cp = cmd.points[1]; - x1 = x0 + (2 / 3) * (cp.x - x0); - y1 = y0 + (2 / 3) * (cp.y - y0); - x2 = x3 + (2 / 3) * (cp.x - x3); - y2 = y3 + (2 / 3) * (cp.y - y3); - } else { - x1 = cmd.points[1].x; - y1 = cmd.points[1].y; - x2 = cmd.points[2].x; - y2 = cmd.points[2].y; - } - area = - (3 * - ((y3 - y0) * (x1 + x2) - - (x3 - x0) * (y1 + y2) + - y1 * (x0 - x2) - - x1 * (y0 - y2) + - y3 * (x2 + x0 / 3) - - x3 * (y2 + y0 / 3))) / - 20; - break; - } - return area; -} diff --git a/src/app/pages/editor/model/paths/PathUtil.ts b/src/app/pages/editor/model/paths/PathUtil.ts deleted file mode 100644 index 5ae0be6b..00000000 --- a/src/app/pages/editor/model/paths/PathUtil.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { MathUtil, Point } from 'app/pages/editor/scripts/common'; - -import { Command } from './Command'; -import { Path } from './Path'; - -/** - * Interpolates between a start and end path using the specified fraction. - * - * TODO: make it possible to create 'stateless' paths (to save memory on animation frames). - */ -export function interpolate(start: Path, end: Path, fraction: number) { - if (!start.isMorphableWith(end)) { - throw new Error('Attempt to interpolate two unmorphable paths'); - } - const newCommands: Command[] = []; - start.getCommands().forEach((startCmd, i) => { - const endCmd = end.getCommands()[i]; - const points: Point[] = []; - for (let j = 0; j < startCmd.points.length; j++) { - const p1 = startCmd.points[j]; - const p2 = endCmd.points[j]; - if (p1 && p2) { - // The 'start' point of the first Move command in a path - // will be undefined. Skip it. - const px = MathUtil.lerp(p1.x, p2.x, fraction); - const py = MathUtil.lerp(p1.y, p2.y, fraction); - points.push({ x: px, y: py }); - } else { - points.push(undefined); - } - } - // TODO: avoid re-generating unique ids on each animation frame. - newCommands.push(new Command(startCmd.type, points)); - }); - return new Path(newCommands); -} - -/** - * Sorts a list of path ops in descending order. - */ -export function sortPathOps(ops: Array<{ subIdx: number; cmdIdx: number }>) { - return ops.sort(({ subIdx: s1, cmdIdx: c1 }, { subIdx: s2, cmdIdx: c2 }) => { - // Perform higher index splits first so that we don't alter the - // indices of the lower index split operations. - return s1 !== s2 ? s2 - s1 : c2 - c1; - }); -} diff --git a/src/app/pages/editor/model/paths/SubPath.ts b/src/app/pages/editor/model/paths/SubPath.ts deleted file mode 100644 index 823b6e77..00000000 --- a/src/app/pages/editor/model/paths/SubPath.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { MathUtil } from 'app/pages/editor/scripts/common'; -import * as _ from 'lodash'; - -import { Command } from '.'; - -/** - * Represents a string of Commands, beginning with a 'moveTo' command and ending - * with either a 'closepath' command or the next 'moveTo' command. - */ -export class SubPath { - constructor( - private readonly commands: ReadonlyArray, - private readonly id = _.uniqueId(), - private readonly isCollapsing_ = false, - private readonly isReversed_ = false, - private readonly shiftOffset = 0, - private readonly isSplit_ = false, - private readonly isUnsplittable_ = false, - ) {} - - /** - * Returns a unique ID for this subpath. - */ - getId() { - return this.id; - } - - /** - * The list of commands in this subpath. - */ - getCommands() { - return this.commands; - } - - /** - * Returns true iff this sub path was created to collapse to a single point. - */ - isCollapsing() { - return this.isCollapsing_; - } - - /** - * Returns true iff this sub path has been reversed. - */ - isReversed() { - return this.isReversed_; - } - - /** - * Returns the shift offset of this sub path. - */ - getShiftOffset() { - return this.shiftOffset; - } - - /** - * Returns true iff this sub path was created as a result of a split. - */ - isSplit() { - return this.isSplit_; - } - - /** - * Returns true iff this sub path can be unsplit. - */ - isUnsplittable() { - return this.isUnsplittable_; - } - - /** - * Returns true iff the sub path's start point is equal to its end point. - */ - isClosed() { - const start = _.first(this.getCommands()).end; - const end = _.last(this.getCommands()).end; - return MathUtil.arePointsEqual(start, end); - } - - /** - * Returns a builder to construct a mutated SubPath. - */ - mutate() { - return new SubPathBuilder( - this.commands, - this.id, - this.isCollapsing_, - this.isReversed_, - this.shiftOffset, - this.isSplit_, - this.isUnsplittable_, - ); - } -} - -export function createSubPaths(commands: ReadonlyArray) { - if (!commands.length || commands[0].type !== 'M') { - // TODO: is this case actually possible? should we insert 'M 0 0' instead? - return []; - } - - let currentCmdList: Command[] = []; - let lastSeenMove: Command; - const subPathCmds: SubPath[] = []; - for (const cmd of commands) { - if (cmd.type === 'M') { - lastSeenMove = cmd; - if (currentCmdList.length) { - subPathCmds.push(new SubPath(currentCmdList)); - currentCmdList = []; - } else { - currentCmdList.push(cmd); - } - continue; - } - if (!currentCmdList.length) { - currentCmdList.push( - lastSeenMove - .mutate() - .setId(_.uniqueId()) - .build(), - ); - } - currentCmdList.push(cmd); - if (cmd.type === 'Z') { - subPathCmds.push(new SubPath(currentCmdList)); - currentCmdList = []; - } - } - if (currentCmdList.length) { - subPathCmds.push(new SubPath(currentCmdList)); - } - return subPathCmds; -} - -export class SubPathBuilder { - constructor( - private commands: ReadonlyArray, - private id: string, - private isCollapsing: boolean, - private isReversed: boolean, - private shiftOffset: number, - private isSplit: boolean, - private isUnsplittable: boolean, - ) {} - - setCommands(commands: Command[]) { - this.commands = commands; - return this; - } - - setId(id: string) { - this.id = id; - return this; - } - - setIsCollapsing(isCollapsing: boolean) { - this.isCollapsing = isCollapsing; - return this; - } - - setIsReversed(isReversed: boolean) { - this.isReversed = isReversed; - return this; - } - - setShiftOffset(shiftOffset: number) { - this.shiftOffset = shiftOffset; - return this; - } - - setIsSplit(isSplit: boolean) { - this.isSplit = isSplit; - return this; - } - - setIsUnsplittable(isUnsplittable: boolean) { - this.isUnsplittable = isUnsplittable; - return this; - } - - build() { - return new SubPath( - [...this.commands], - this.id, - this.isCollapsing, - this.isReversed, - this.shiftOffset, - this.isSplit, - this.isUnsplittable, - ); - } -} diff --git a/src/app/pages/editor/model/paths/SubPathState.ts b/src/app/pages/editor/model/paths/SubPathState.ts deleted file mode 100644 index daa748a5..00000000 --- a/src/app/pages/editor/model/paths/SubPathState.ts +++ /dev/null @@ -1,149 +0,0 @@ -import * as _ from 'lodash'; - -import { CommandState } from './CommandState'; - -/** - * Container class that encapsulates a SubPath's underlying state. - */ -export class SubPathState { - constructor( - private readonly commandStates: ReadonlyArray, - private readonly isReversed_ = false, - private readonly shiftOffset = 0, - private readonly id = _.uniqueId(), - // Either empty if this sub path is not split, or an array - // containing this sub path's split children. - private readonly splitSubPaths: ReadonlyArray = [], - ) {} - - getId() { - return this.id; - } - - getCommandStates() { - return this.commandStates; - } - - isReversed() { - return this.isReversed_; - } - - getShiftOffset() { - return this.shiftOffset; - } - - getSplitSubPaths() { - return this.splitSubPaths; - } - - revert() { - return this.mutate() - .revert() - .build(); - } - - clone() { - return this.mutate().build(); - } - - mutate() { - return new SubPathStateMutator( - [...this.commandStates], - this.isReversed_, - this.shiftOffset, - this.id, - [...this.splitSubPaths], - ); - } -} - -/** - * Builder class for creating new SubPathState objects. - */ -export class SubPathStateMutator { - constructor( - private commandStates: CommandState[], - private isReversed: boolean, - private shiftOffset: number, - private id: string, - private splitSubPaths: ReadonlyArray, - ) {} - - setCommandStates(commandStates: CommandState[]) { - this.commandStates = [...commandStates]; - return this; - } - - setCommandState(index: number, commandState: CommandState) { - if (!this.commandStates || this.commandStates.length <= index) { - throw new Error('Attempt to set a CommandState object using an invalid index'); - } - this.commandStates[index] = commandState; - return this; - } - - reverse() { - return this.setIsReversed(!this.isReversed); - } - - setIsReversed(isReversed: boolean) { - this.isReversed = isReversed; - return this; - } - - setShiftOffset(shiftOffset: number) { - this.shiftOffset = shiftOffset; - return this; - } - - setSplitSubPaths(splitSubPaths: SubPathState[]) { - this.splitSubPaths = [...splitSubPaths]; - return this; - } - - setId(id: string) { - this.id = id; - return this; - } - - revert() { - this.commandStates = this.commandStates.map(cs => - cs - .mutate() - .revert() - .build(), - ); - this.isReversed = false; - this.shiftOffset = 0; - this.splitSubPaths = []; - return this; - } - - build() { - return new SubPathState( - this.commandStates, - this.isReversed, - this.shiftOffset, - this.id, - this.splitSubPaths, - ); - } -} - -export function findSubPathState(map: ReadonlyArray, spsIdx: number) { - return flattenSubPathStates(map)[spsIdx]; -} - -export function flattenSubPathStates(map: ReadonlyArray) { - const subPathStates: SubPathState[] = []; - (function recurseFn(currentLevel: ReadonlyArray) { - currentLevel.forEach(state => { - if (!state.getSplitSubPaths().length) { - subPathStates.push(state); - return; - } - recurseFn(state.getSplitSubPaths()); - }); - })(map); - return subPathStates; -} diff --git a/src/app/pages/editor/model/paths/SvgChar.ts b/src/app/pages/editor/model/paths/SvgChar.ts deleted file mode 100644 index bc225f16..00000000 --- a/src/app/pages/editor/model/paths/SvgChar.ts +++ /dev/null @@ -1,4 +0,0 @@ -/** - * The different types of supported SVG commands. - */ -export type SvgChar = 'M' | 'L' | 'Q' | 'C' | 'Z'; diff --git a/src/app/pages/editor/model/paths/SvgUtil.ts b/src/app/pages/editor/model/paths/SvgUtil.ts deleted file mode 100644 index ade2577a..00000000 --- a/src/app/pages/editor/model/paths/SvgUtil.ts +++ /dev/null @@ -1,179 +0,0 @@ -interface EllipticalArc { - startX?: number; - startY?: number; - rx: number; - ry: number; - xAxisRotation: number; - largeArcFlag: number; - sweepFlag: number; - endX: number; - endY: number; -} - -/** Estimates an elliptical arc as a sequence of bezier curves. */ -export function arcToBeziers(arc: EllipticalArc) { - const { startX: xf, startY: yf, largeArcFlag, sweepFlag, endX: xt, endY: yt } = arc; - let rx = arc.rx; - let ry = arc.ry; - let xAxisRotation = arc.xAxisRotation; - - // Sign of the radii is ignored (behaviour specified by the spec) - rx = Math.abs(rx); - ry = Math.abs(ry); - - xAxisRotation = xAxisRotation * Math.PI / 180; - const cosAngle = Math.cos(xAxisRotation); - const sinAngle = Math.sin(xAxisRotation); - - // We simplify the calculations by transforming the arc so that the origin is at the - // midpoint calculated above followed by a rotation to line up the coordinate axes - // with the axes of the ellipse. - - // Compute the midpoint of the line between the current and the end point - const dx2 = (xf - xt) / 2; - const dy2 = (yf - yt) / 2; - - // Step 1 : Compute (x1', y1') - the transformed start point - const x1 = cosAngle * dx2 + sinAngle * dy2; - const y1 = -sinAngle * dx2 + cosAngle * dy2; - - let rx_sq = rx * rx; - let ry_sq = ry * ry; - const x1_sq = x1 * x1; - const y1_sq = y1 * y1; - - // Check that radii are large enough. - // If they are not, the spec says to scale them up so they are. - // This is to compensate for potential rounding errors/differences between SVG implementations. - const radiiCheck = x1_sq / rx_sq + y1_sq / ry_sq; - if (radiiCheck > 1) { - rx = Math.sqrt(radiiCheck) * rx; - ry = Math.sqrt(radiiCheck) * ry; - rx_sq = rx * rx; - ry_sq = ry * ry; - } - - // Step 2 : Compute (cx1, cy1) - the transformed centre point - let sign = largeArcFlag === sweepFlag ? -1 : 1; - let sq = (rx_sq * ry_sq - rx_sq * y1_sq - ry_sq * x1_sq) / (rx_sq * y1_sq + ry_sq * x1_sq); - sq = sq < 0 ? 0 : sq; - const coef = sign * Math.sqrt(sq); - const cx1 = coef * (rx * y1 / ry); - const cy1 = coef * -(ry * x1 / rx); - - // Step 3 : Compute (cx, cy) from (cx1, cy1) - const sx2 = (xf + xt) / 2; - const sy2 = (yf + yt) / 2; - const cx = sx2 + (cosAngle * cx1 - sinAngle * cy1); - const cy = sy2 + (sinAngle * cx1 + cosAngle * cy1); - - // Step 4 : Compute the angleStart (angle1) and the angleExtent (dangle) - const ux = (x1 - cx1) / rx; - const uy = (y1 - cy1) / ry; - const vx = (-x1 - cx1) / rx; - const vy = (-y1 - cy1) / ry; - let p, n; - - // Compute the angle start - n = Math.sqrt(ux * ux + uy * uy); - p = ux; // (1 * ux) + (0 * uy) - sign = uy < 0 ? -1 : 1; - let angleStart = sign * Math.acos(p / n) * 180 / Math.PI; - - // Compute the angle extent - n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)); - p = ux * vx + uy * vy; - sign = ux * vy - uy * vx < 0 ? -1 : 1; - let angleExtent = sign * Math.acos(p / n) * 180 / Math.PI; - if (!sweepFlag && angleExtent > 0) { - angleExtent -= 360; - } else if (sweepFlag && angleExtent < 0) { - angleExtent += 360; - } - - angleExtent %= 360; - angleStart %= 360; - - // Many elliptical arc implementations including the Java2D and Android ones, only - // support arcs that are axis aligned. Therefore we need to substitute the arc - // with bezier curves. The following method call will generate the beziers for - // a unit circle that covers the arc angles we want. - const bezierCoords = unitCircleArcToBeziers(angleStart, angleExtent); - - // Calculate a transformation matrix that will move and scale these bezier points to the correct location. - // translate(cx, cy) --> rotate(rotate) --> scale(rx, ry) - for (let i = 0; i < bezierCoords.length; i += 2) { - // dot product - const x = bezierCoords[i]; - const y = bezierCoords[i + 1]; - bezierCoords[i] = cosAngle * rx * x + -sinAngle * ry * y + cx; - - bezierCoords[i + 1] = sinAngle * rx * x + cosAngle * ry * y + cy; - } - - // The last point in the bezier set should match exactly the last coord pair in the arc (ie: x,y). But - // considering all the mathematical manipulation we have been doing, it is bound to be off by a tiny - // fraction. Experiments show that it can be up to around 0.00002. So why don't we just set it to - // exactly what it ought to be. - bezierCoords[bezierCoords.length - 2] = xt; - bezierCoords[bezierCoords.length - 1] = yt; - return bezierCoords; -} - -/* -* Generate the control points and endpoints for a set of bezier curves that match -* a circular arc starting from angle 'angleStart' and sweep the angle 'angleExtent'. -* The circle the arc follows will be centred on (0,0) and have a radius of 1.0. -* -* Each bezier can cover no more than 90 degrees, so the arc will be divided evenly -* into a maximum of four curves. -* -* The resulting control points will later be scaled and rotated to match the final -* arc required. -* -* The returned array has the format [x0,y0, x1,y1,...]. -*/ -function unitCircleArcToBeziers(angleStart: number, angleExtent: number): number[] { - const numSegments = Math.ceil(Math.abs(angleExtent) / 90); - - angleStart = angleStart * Math.PI / 180; - angleExtent = angleExtent * Math.PI / 180; - - const angleIncrement = angleExtent / numSegments; - - // The length of each control point vector is given by the following formula. - const controlLength = 4 / 3 * Math.sin(angleIncrement / 2) / (1 + Math.cos(angleIncrement / 2)); - - const coords = new Array(numSegments * 8); - let pos = 0; - - for (let i = 0; i < numSegments; i++) { - let angle = angleStart + i * angleIncrement; - - // Calculate the control vector at this angle - let dx = Math.cos(angle); - let dy = Math.sin(angle); - - // First point - coords[pos++] = dx; - coords[pos++] = dy; - - // First control point - coords[pos++] = dx - controlLength * dy; - coords[pos++] = dy + controlLength * dx; - - // Second control point - angle += angleIncrement; - dx = Math.cos(angle); - dy = Math.sin(angle); - - coords[pos++] = dx + controlLength * dy; - coords[pos++] = dy - controlLength * dx; - - // Endpoint of bezier - coords[pos++] = dx; - coords[pos++] = dy; - } - - return coords; -} diff --git a/src/app/pages/editor/model/paths/calculators/BezierCalculator.ts b/src/app/pages/editor/model/paths/calculators/BezierCalculator.ts deleted file mode 100644 index f5f4d9d2..00000000 --- a/src/app/pages/editor/model/paths/calculators/BezierCalculator.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Projection, SvgChar } from 'app/pages/editor/model/paths'; -import { CommandBuilder } from 'app/pages/editor/model/paths/Command'; -import { MathUtil, Point } from 'app/pages/editor/scripts/common'; -import * as BezierJs from 'bezier-js'; -import { environment } from 'environments/environment'; -import * as _ from 'lodash'; - -import { BBox, Calculator, Line } from '.'; -import { LineCalculator } from './LineCalculator'; -import { PointCalculator } from './PointCalculator'; - -/** - * A simple typed wrapper class around the amazing bezier-js library. - */ -export class BezierCalculator implements Calculator { - private readonly points: ReadonlyArray; - private length: number; - private bbox: BBox; - private bezierJs_: any; - - constructor(private readonly id: string, private readonly svgChar: SvgChar, ...points: Point[]) { - this.points = points; - - // Don't initialize variables lazily for dev builds (to avoid - // ngrx-store-freeze crashes). - if (!environment.production) { - this.getPathLength(); - this.getBoundingBox(); - } - } - - private get bezierJs() { - if (this.bezierJs_ === undefined) { - this.bezierJs_ = new BezierJs(this.points); - } - return this.bezierJs_; - } - - getPointAtLength(distance: number) { - return this.bezierJs.get(this.findTimeByDistance(distance / this.getPathLength())) as Point; - } - - getPathLength() { - if (this.length === undefined) { - this.length = this.bezierJs.length(); - } - return this.length; - } - - project(point: Point): Projection { - // Create a new bezier curve for dev builds to avoid ngrx-store-freeze crashes. - const bezierJs = !environment.production ? new BezierJs(this.points) : this.bezierJs; - const { x, y, t, d } = bezierJs.project(point); - return { x, y, t, d }; - } - - split(t1: number, t2: number): Calculator { - if (t1 === t2) { - return new PointCalculator(this.id, this.svgChar, this.bezierJs.get(t1) as Point); - } - const points: ReadonlyArray = this.bezierJs.split(t1, t2).points; - const uniquePoints: Point[] = _.uniqWith(points, MathUtil.arePointsEqual); - if (uniquePoints.length === 2) { - return new LineCalculator(this.id, this.svgChar, _.first(points), _.last(points)); - } - return new BezierCalculator(this.id, this.svgChar, ...points); - } - - convert(svgChar: SvgChar) { - if (svgChar === undefined) { - throw new Error('Attempt to convert an undefined svgChar'); - } - if (this.svgChar === 'Q' && svgChar === 'C') { - const qcp0 = this.points[0]; - const qcp1 = this.points[1]; - const qcp2 = this.points[2]; - const ccp0 = qcp0; - const ccp1 = { - x: qcp0.x + (2 / 3) * (qcp1.x - qcp0.x), - y: qcp0.y + (2 / 3) * (qcp1.y - qcp0.y), - }; - const ccp2 = { - x: qcp2.x + (2 / 3) * (qcp1.x - qcp2.x), - y: qcp2.y + (2 / 3) * (qcp1.y - qcp2.y), - }; - const ccp3 = qcp2; - return new BezierCalculator(this.id, svgChar, ccp0, ccp1, ccp2, ccp3); - } - return new BezierCalculator(this.id, svgChar, ...this.points); - } - - findTimeByDistance(distance: number): number { - if (distance < 0 || distance > 1) { - console.warn('distance must be a number between 0 and 1.'); - } - if (distance === 0 || distance === 1) { - return distance; - } - const originalDistance = distance; - const epsilon = 0.001; - const maxDepth = -100; - - const lowToHighRatio = distance / (1 - distance); - let step = -2; - while (step > maxDepth) { - const split = this.bezierJs.split(distance); - const low = split.left.length(); - const high = split.right.length(); - const diff = low - lowToHighRatio * high; - if (Math.abs(diff) < epsilon) { - // We found a satisfactory midpoint t value. - break; - } - // Jump half the t-distance in the direction of the bias. - step = step - 1; - distance += (diff > 0 ? -1 : 1) * 2 ** step; - } - - if (step === maxDepth) { - // TODO: handle degenerate curves!!!!! - console.warn( - 'Could not find the midpoint for: ', - `${this.svgChar} ` + this.points.toString(), - ); - return originalDistance; - } - - return distance; - } - - toCommand() { - return new CommandBuilder(this.svgChar, [...this.points]).setId(this.id).build(); - } - - getBoundingBox() { - if (this.bbox === undefined) { - const bbox = this.bezierJs.bbox(); - this.bbox = { - x: { min: bbox.x.min, max: bbox.x.max }, - y: { min: bbox.y.min, max: bbox.y.max }, - }; - } - return this.bbox; - } - - intersects(line: Line): number[] { - if (MathUtil.arePointsEqual(_.first(this.points), _.last(this.points))) { - // Points can't be intersected. - return []; - } - return this.bezierJs.intersects(line); - } -} diff --git a/src/app/pages/editor/model/paths/calculators/Calculator.ts b/src/app/pages/editor/model/paths/calculators/Calculator.ts deleted file mode 100644 index 661609e9..00000000 --- a/src/app/pages/editor/model/paths/calculators/Calculator.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { MathUtil, Point } from 'app/pages/editor/scripts/common'; -import * as _ from 'lodash'; - -import { Command, SvgChar } from '..'; -import { BezierCalculator } from './BezierCalculator'; -import { LineCalculator } from './LineCalculator'; -import { MoveCalculator } from './MoveCalculator'; -import { PointCalculator } from './PointCalculator'; - -/** - * A wrapper around a backing SVG command that abstracts a lot of the math-y - * path-related code from the rest of the application. - */ -export interface Calculator { - getPathLength(): number; - getPointAtLength(distance: number): Point; - project(point: Point): Projection | undefined; - split(t1: number, t2: number): Calculator; - convert(svgChar: SvgChar): Calculator; - findTimeByDistance(distance: number): number; - toCommand(): Command; - getBoundingBox(): BBox; - intersects(line: Line): number[]; -} - -export function newCalculator(cmd: Command): Calculator { - const points = cmd.points; - if (cmd.type === 'M') { - return new MoveCalculator(cmd.id, points[0], points[1]); - } - const uniquePoints: Point[] = _.uniqWith(points, MathUtil.arePointsEqual); - if (uniquePoints.length === 1) { - return new PointCalculator(cmd.id, cmd.type, points[0]); - } - if (cmd.type === 'L' || cmd.type === 'Z' || uniquePoints.length === 2) { - return new LineCalculator(cmd.id, cmd.type, _.first(points), _.last(points)); - } - if (cmd.type === 'Q') { - return new BezierCalculator(cmd.id, cmd.type, points[0], points[1], points[2]); - } - if (cmd.type === 'C') { - const pts = cmd.points; - return new BezierCalculator(cmd.id, cmd.type, pts[0], pts[1], pts[2], pts[3]); - } - throw new Error('Invalid command type: ' + cmd.type); -} - -/** Represents a projection onto a path. */ -export interface Projection { - /** The x-coordinate of the point on the path. */ - x: number; - /** The y-coordinate of the point on the path. */ - y: number; - /** The t-value of the point on the path. */ - t: number; - /** The distance of the source point to the point on the path. */ - d: number; -} - -/** Represents a rectangular bounding box. */ -export interface BBox { - x: MinMax; - y: MinMax; -} - -interface MinMax { - min: number; - max: number; -} - -/** Represents a 2D line. */ -export interface Line { - p1: Point; - p2: Point; -} diff --git a/src/app/pages/editor/model/paths/calculators/LineCalculator.ts b/src/app/pages/editor/model/paths/calculators/LineCalculator.ts deleted file mode 100644 index 4db240dc..00000000 --- a/src/app/pages/editor/model/paths/calculators/LineCalculator.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { Projection, SvgChar } from 'app/pages/editor/model/paths'; -import { CommandBuilder } from 'app/pages/editor/model/paths/Command'; -import { MathUtil, Point } from 'app/pages/editor/scripts/common'; -import * as _ from 'lodash'; - -import { BBox, Calculator, Line } from '.'; -import { PointCalculator } from './PointCalculator'; - -const ROUNDING_PRECISION = 10; - -export class LineCalculator implements Calculator { - constructor( - private readonly id: string, - private readonly svgChar: SvgChar, - private readonly p1: Point, - private readonly p2: Point, - ) {} - - getPathLength() { - return MathUtil.distance(this.p1, this.p2); - } - - getPointAtLength(distance: number): Point { - const t = distance / this.getPathLength(); - const { x: x1, y: y1 } = this.p1; - const { x: x2, y: y2 } = this.p2; - return { - x: MathUtil.lerp(x1, x2, t), - y: MathUtil.lerp(y1, y2, t), - }; - } - - project({ x, y }: Point): Projection { - const { x: x1, y: y1 } = this.p1; - const { x: x2, y: y2 } = this.p2; - const a = x2 - x1; - const b = y2 - y1; - const dot = (x - x1) * a + (y - y1) * b; - const lenSq = round(a * a + b * b); - const param = lenSq === 0 ? -1 : round(dot / lenSq); - let xx: number; - let yy: number; - if (param < 0) { - xx = x1; - yy = y1; - } else if (param > 1) { - xx = x2; - yy = y2; - } else { - xx = x1 + param * a; - yy = y1 + param * b; - } - const dx = x - xx; - const dy = y - yy; - const dd = Math.sqrt(dx * dx + dy * dy); - let dt: number; - const rx1 = round(x1); - const rx2 = round(x2); - const ry1 = round(y1); - const ry2 = round(y2); - if (rx2 !== rx1) { - dt = (xx - x1) / (x2 - x1); - } else if (ry2 !== ry1) { - dt = (yy - y1) / (y2 - y1); - } else { - dt = 0.5; - } - return { x: round(xx), y: round(yy), d: round(dd), t: round(dt) }; - } - - split(t1: number, t2: number) { - const { x: x1, y: y1 } = this.p1; - const { x: x2, y: y2 } = this.p2; - const p1 = { x: MathUtil.lerp(x1, x2, t1), y: MathUtil.lerp(y1, y2, t1) }; - const p2 = { x: MathUtil.lerp(x1, x2, t2), y: MathUtil.lerp(y1, y2, t2) }; - if (MathUtil.arePointsEqual(p1, p2)) { - return new PointCalculator(this.id, this.svgChar, p1); - } - return new LineCalculator(this.id, this.svgChar, p1, p2); - } - - convert(svgChar: SvgChar) { - return new LineCalculator(this.id, svgChar, this.p1, this.p2); - } - - findTimeByDistance(distance: number) { - return distance; - } - - toCommand() { - let points: Point[]; - switch (this.svgChar) { - case 'L': - case 'Z': - points = [this.p1, this.p2]; - break; - case 'Q': - const cp = { - x: MathUtil.lerp(this.p1.x, this.p2.x, 0.5), - y: MathUtil.lerp(this.p1.y, this.p2.y, 0.5), - }; - points = [this.p1, cp, this.p2]; - break; - case 'C': - const cp1 = { - x: MathUtil.lerp(this.p1.x, this.p2.x, 1 / 3), - y: MathUtil.lerp(this.p1.y, this.p2.y, 1 / 3), - }; - const cp2 = { - x: MathUtil.lerp(this.p1.x, this.p2.x, 2 / 3), - y: MathUtil.lerp(this.p1.y, this.p2.y, 2 / 3), - }; - points = [this.p1, cp1, cp2, this.p2]; - break; - default: - throw new Error('Invalid command type: ' + this.svgChar); - } - return new CommandBuilder(this.svgChar, points).setId(this.id).build(); - } - - getBoundingBox() { - const minx = Math.min(this.p1.x, this.p2.x); - const miny = Math.min(this.p1.y, this.p2.y); - const maxx = Math.max(this.p1.x, this.p2.x); - const maxy = Math.max(this.p1.y, this.p2.y); - return { x: { min: minx, max: maxx }, y: { min: miny, max: maxy } } as BBox; - } - - intersects(line: Line) { - if (MathUtil.arePointsEqual(this.p1, this.p2)) { - // Points can't be intersected. - return []; - } - - // Check to see if the line from (a,b) to (c,d) intersects - // with the line from (p,q) to (r,s). - const { x: a, y: b } = this.p1; - const { x: c, y: d } = this.p2; - const { - p1: { x: p, y: q }, - p2: { x: r, y: s }, - } = line; - const det = round((c - a) * (s - q) - (r - p) * (d - b)); - if (det === 0) { - // Then the two lines are parallel. In our case it is fine to - // return an empty list, even though the lines may technically - // be collinear. - return []; - } else { - const t = round(((s - q) * (r - a) + (p - r) * (s - b)) / det); - const u = round(((b - d) * (r - a) + (c - a) * (s - b)) / det); - return 0 <= t && t <= 1 && (0 <= u && u <= 1) ? [t] : []; - } - } -} - -function round(num: number) { - return _.round(num, ROUNDING_PRECISION); -} diff --git a/src/app/pages/editor/model/paths/calculators/MoveCalculator.ts b/src/app/pages/editor/model/paths/calculators/MoveCalculator.ts deleted file mode 100644 index a2ff2867..00000000 --- a/src/app/pages/editor/model/paths/calculators/MoveCalculator.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Projection, SvgChar } from 'app/pages/editor/model/paths'; -import { CommandBuilder } from 'app/pages/editor/model/paths/Command'; -import { Point } from 'app/pages/editor/scripts/common'; - -import { BBox, Calculator, Line } from '.'; - -export class MoveCalculator implements Calculator { - constructor( - private readonly id: string, - private readonly startPoint: Point | undefined, - private readonly endPoint: Point, - ) {} - - getPathLength() { - return 0; - } - - project(point: Point): Projection | undefined { - return undefined; - } - - getPointAtLength(distance: number): Point { - return this.endPoint; - } - - split(t1: number, t2: number): Calculator { - return this; - } - - convert(svgChar: SvgChar) { - return this; - } - - findTimeByDistance(distance: number) { - return distance; - } - - toCommand() { - return new CommandBuilder('M', [this.startPoint, this.endPoint]).setId(this.id).build(); - } - - getBoundingBox() { - const x = { min: NaN, max: NaN }; - const y = { min: NaN, max: NaN }; - return { x, y } as BBox; - } - - intersects(line: Line): number[] { - return []; - } -} diff --git a/src/app/pages/editor/model/paths/calculators/PointCalculator.ts b/src/app/pages/editor/model/paths/calculators/PointCalculator.ts deleted file mode 100644 index a1c4420e..00000000 --- a/src/app/pages/editor/model/paths/calculators/PointCalculator.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Projection, SvgChar } from 'app/pages/editor/model/paths'; -import { CommandBuilder } from 'app/pages/editor/model/paths/Command'; -import { MathUtil, Point } from 'app/pages/editor/scripts/common'; - -import { BBox, Calculator, Line } from '.'; - -export class PointCalculator implements Calculator { - private readonly svgChar: SvgChar; - private readonly point: Point; - - constructor(private readonly id: string, svgChar: SvgChar, point: Point) { - this.svgChar = svgChar; - this.point = point; - } - - getPathLength() { - return 0; - } - - getPointAtLength(distance: number) { - return this.point; - } - - project(point: Point) { - const x = this.point.x; - const y = this.point.y; - const t = 0.5; - const d = MathUtil.distance(this.point, point); - return { x, y, t, d } as Projection; - } - - split(t1: number, t2: number) { - return new PointCalculator(this.id, this.svgChar, this.point); - } - - convert(svgChar: SvgChar) { - return new PointCalculator(this.id, svgChar, this.point); - } - - findTimeByDistance(distance: number) { - return distance; - } - - toCommand() { - let points; - switch (this.svgChar) { - case 'L': - case 'Z': - points = [this.point, this.point]; - break; - case 'Q': - points = [this.point, this.point, this.point]; - break; - case 'C': - points = [this.point, this.point, this.point, this.point]; - break; - default: - throw new Error('Invalid command type: ' + this.svgChar); - } - return new CommandBuilder(this.svgChar, points).setId(this.id).build(); - } - - getBoundingBox() { - const x = { min: this.point.x, max: this.point.x }; - const y = { min: this.point.y, max: this.point.y }; - return { x, y } as BBox; - } - - intersects(line: Line): number[] { - return []; - } -} diff --git a/src/app/pages/editor/model/paths/calculators/index.ts b/src/app/pages/editor/model/paths/calculators/index.ts deleted file mode 100644 index 6a492c30..00000000 --- a/src/app/pages/editor/model/paths/calculators/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Calculator, newCalculator, Projection, BBox, Line } from './Calculator'; diff --git a/src/app/pages/editor/model/paths/index.ts b/src/app/pages/editor/model/paths/index.ts deleted file mode 100644 index 593f961e..00000000 --- a/src/app/pages/editor/model/paths/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as PathUtil from './PathUtil'; - -export { PathUtil }; -export { Projection, Line } from './calculators'; -export { SvgChar } from './SvgChar'; -export { Path, HitOptions, HitResult, ProjectionOntoPath, PathMutator } from './Path'; -export { SubPath } from './SubPath'; -export { Command } from './Command'; diff --git a/src/app/pages/editor/model/properties/ColorProperty.ts b/src/app/pages/editor/model/properties/ColorProperty.ts deleted file mode 100644 index 9a4199b5..00000000 --- a/src/app/pages/editor/model/properties/ColorProperty.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ColorUtil, MathUtil } from 'app/pages/editor/scripts/common'; -import * as _ from 'lodash'; - -import { Property } from './Property'; - -export class ColorProperty extends Property { - // @Override - setEditableValue(model: any, propertyName: string, value: string) { - if (!value) { - model[propertyName] = undefined; - return; - } - let processedValue = ColorUtil.parseAndroidColor(value); - if (!processedValue) { - processedValue = ColorUtil.parseAndroidColor(ColorUtil.svgToAndroidColor(value)); - } - model[propertyName] = ColorUtil.toAndroidString(processedValue); - } - - // @Override - interpolateValue(start: string, end: string, f: number) { - if (!start || !end) { - return undefined; - } - const s = ColorUtil.parseAndroidColor(start); - const e = ColorUtil.parseAndroidColor(end); - return ColorUtil.toAndroidString({ - r: _.clamp(Math.round(MathUtil.lerp(s.r, e.r, f)), 0, 0xff), - g: _.clamp(Math.round(MathUtil.lerp(s.g, e.g, f)), 0, 0xff), - b: _.clamp(Math.round(MathUtil.lerp(s.b, e.b, f)), 0, 0xff), - a: _.clamp(Math.round(MathUtil.lerp(s.a, e.a, f)), 0, 0xff), - }); - } - - // @Override - getAnimatorValueType() { - return 'colorType'; - } - - // @Override - getTypeName() { - return 'ColorProperty'; - } -} diff --git a/src/app/pages/editor/model/properties/EnumProperty.ts b/src/app/pages/editor/model/properties/EnumProperty.ts deleted file mode 100644 index dc15c2de..00000000 --- a/src/app/pages/editor/model/properties/EnumProperty.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as _ from 'lodash'; - -import { Property } from './Property'; - -export class EnumProperty extends Property { - constructor(name: string, readonly options: ReadonlyArray
').css({ - position: 'fixed', - left: 0, - top: 0, - right: 0, - bottom: 0, - zIndex: 9999, - }); - } -} - -type Direction = 'horizontal' | 'vertical' | 'both'; - -interface ConstructorArgs { - direction?: Direction; - downX?: number; - downY?: number; - shouldSkipSlopCheck?: boolean; - onBeginDragFn?: (event: JQuery.Event) => void; - onDragFn?: (event: JQuery.Event, point: Point) => void; - onDropFn?: () => void; - draggingCursor?: string; -} diff --git a/src/app/pages/editor/scripts/dragger/index.ts b/src/app/pages/editor/scripts/dragger/index.ts deleted file mode 100644 index f0644dfb..00000000 --- a/src/app/pages/editor/scripts/dragger/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Dragger } from './Dragger'; diff --git a/src/app/pages/editor/scripts/export/AvdSerializer.ts b/src/app/pages/editor/scripts/export/AvdSerializer.ts deleted file mode 100644 index 401d14ab..00000000 --- a/src/app/pages/editor/scripts/export/AvdSerializer.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { INTERPOLATORS } from 'app/pages/editor/model/interpolators'; -import { - ClipPathLayer, - GroupLayer, - Layer, - PathLayer, - VectorLayer, -} from 'app/pages/editor/model/layers'; -import { Animation, AnimationBlock, PathAnimationBlock } from 'app/pages/editor/model/timeline'; -import * as _ from 'lodash'; - -import * as XmlSerializer from './XmlSerializer'; - -const XMLNS_NS = 'http://www.w3.org/2000/xmlns/'; -const ANDROID_NS = 'http://schemas.android.com/apk/res/android'; -const AAPT_NS = 'http://schemas.android.com/aapt'; - -/** - * Serializes a VectorLayer to a vector drawable XML string. - */ -export function toVectorDrawableXmlString(vl: VectorLayer) { - const xmlDoc = document.implementation.createDocument(undefined, 'vector', undefined); - const rootNode = xmlDoc.documentElement; - vectorLayerToXmlNode(vl, rootNode, xmlDoc); - return serializeXmlNode(rootNode); -} - -/** - * Serializes a given VectorLayer and Animation to an animatedvector drawable XML file. - */ -export function toAnimatedVectorDrawableXmlString(vl: VectorLayer, animation: Animation) { - const xmlDoc = document.implementation.createDocument(undefined, 'animated-vector', undefined); - const rootNode = xmlDoc.documentElement; - rootNode.setAttributeNS(XMLNS_NS, 'xmlns:android', ANDROID_NS); - rootNode.setAttributeNS(XMLNS_NS, 'xmlns:aapt', AAPT_NS); - - // Create drawable node containing the vector layer. - const vectorLayerContainerNode = xmlDoc.createElementNS(AAPT_NS, 'aapt:attr'); - vectorLayerContainerNode.setAttribute('name', 'android:drawable'); - rootNode.appendChild(vectorLayerContainerNode); - - const vectorLayerNode = xmlDoc.createElement('vector'); - vectorLayerToXmlNode(vl, vectorLayerNode, xmlDoc, false); - vectorLayerContainerNode.appendChild(vectorLayerNode); - - // create animation nodes (one per layer) - const animBlocksByLayer = new Map(); - animation.blocks.forEach(block => { - const blocks = animBlocksByLayer.get(block.layerId) || []; - blocks.push(block); - animBlocksByLayer.set(block.layerId, blocks); - }); - - animBlocksByLayer.forEach((blocksForLayer, layerId) => { - const targetNode = xmlDoc.createElement('target'); - const layer = vl.findLayerById(layerId); - targetNode.setAttributeNS(ANDROID_NS, 'android:name', layer.name); - rootNode.appendChild(targetNode); - - const animationNode = xmlDoc.createElementNS(AAPT_NS, 'aapt:attr'); - animationNode.setAttribute('name', 'android:animation'); - targetNode.appendChild(animationNode); - - let blockContainerNode = animationNode; - if (blocksForLayer.length > 1) { - // for multiple property animations on a single layer. - blockContainerNode = xmlDoc.createElement('set'); - animationNode.appendChild(blockContainerNode); - } - - const animatableProperties = layer.animatableProperties; - - blocksForLayer.forEach(block => { - const blockNode = xmlDoc.createElement('objectAnimator'); - blockNode.setAttributeNS(ANDROID_NS, 'android:propertyName', block.propertyName); - conditionalAttrFn(blockNode, 'android:startOffset', block.startTime, 0); - conditionalAttrFn(blockNode, 'android:duration', block.endTime - block.startTime); - if (block instanceof PathAnimationBlock) { - const fromPath = block.fromValue; - const toPath = block.toValue; - conditionalAttrFn(blockNode, 'android:valueFrom', fromPath ? fromPath.getPathString() : ''); - conditionalAttrFn(blockNode, 'android:valueTo', toPath ? toPath.getPathString() : ''); - } else { - conditionalAttrFn(blockNode, 'android:valueFrom', block.fromValue); - conditionalAttrFn(blockNode, 'android:valueTo', block.toValue); - } - conditionalAttrFn( - blockNode, - 'android:valueType', - animatableProperties.get(block.propertyName).getAnimatorValueType(), - ); - const interpolator = _.find(INTERPOLATORS, i => i.value === block.interpolator); - conditionalAttrFn(blockNode, 'android:interpolator', interpolator.androidRef); - blockContainerNode.appendChild(blockNode); - }); - }); - return serializeXmlNode(rootNode); -} - -/** - * Helper method that serializes an VectorLayer to a destinationNode in an xmlDoc. - * The destinationNode should be a node. - */ -function vectorLayerToXmlNode( - vl: VectorLayer, - destinationNode: any, - xmlDoc: any, - withAndroidNs = true, -) { - if (withAndroidNs) { - destinationNode.setAttributeNS(XMLNS_NS, 'xmlns:android', ANDROID_NS); - } - conditionalAttrFn(destinationNode, 'android:name', vl.name); - destinationNode.setAttributeNS(ANDROID_NS, 'android:width', `${vl.width}dp`); - destinationNode.setAttributeNS(ANDROID_NS, 'android:height', `${vl.height}dp`); - destinationNode.setAttributeNS(ANDROID_NS, 'android:viewportWidth', `${vl.width}`); - destinationNode.setAttributeNS(ANDROID_NS, 'android:viewportHeight', `${vl.height}`); - conditionalAttrFn(destinationNode, 'android:alpha', vl.alpha, 1); - - walk( - vl, - (layer: any, parentNode: any) => { - if (layer instanceof VectorLayer) { - return parentNode; - } else if (layer instanceof PathLayer) { - const node = xmlDoc.createElement('path'); - const path = layer.pathData; - conditionalAttrFn(node, 'android:name', layer.name); - conditionalAttrFn(node, 'android:pathData', path ? path.getPathString() : ''); - conditionalAttrFn(node, 'android:fillColor', layer.fillColor, ''); - conditionalAttrFn(node, 'android:fillAlpha', layer.fillAlpha, 1); - conditionalAttrFn(node, 'android:strokeColor', layer.strokeColor, ''); - conditionalAttrFn(node, 'android:strokeAlpha', layer.strokeAlpha, 1); - conditionalAttrFn(node, 'android:strokeWidth', layer.strokeWidth, 0); - conditionalAttrFn(node, 'android:trimPathStart', layer.trimPathStart, 0); - conditionalAttrFn(node, 'android:trimPathEnd', layer.trimPathEnd, 1); - conditionalAttrFn(node, 'android:trimPathOffset', layer.trimPathOffset, 0); - conditionalAttrFn(node, 'android:strokeLineCap', layer.strokeLinecap, 'butt'); - conditionalAttrFn(node, 'android:strokeLineJoin', layer.strokeLinejoin, 'miter'); - conditionalAttrFn(node, 'android:strokeMiterLimit', layer.strokeMiterLimit, 4); - conditionalAttrFn(node, 'android:fillType', layer.fillType, 'nonZero'); - parentNode.appendChild(node); - return parentNode; - } else if (layer instanceof ClipPathLayer) { - const node = xmlDoc.createElement('clip-path'); - const path = layer.pathData; - conditionalAttrFn(node, 'android:name', layer.name); - conditionalAttrFn(node, 'android:pathData', path ? path.getPathString() : ''); - parentNode.appendChild(node); - return parentNode; - } else if (layer instanceof GroupLayer) { - const node = xmlDoc.createElement('group'); - conditionalAttrFn(node, 'android:name', layer.name); - conditionalAttrFn(node, 'android:pivotX', layer.pivotX, 0); - conditionalAttrFn(node, 'android:pivotY', layer.pivotY, 0); - conditionalAttrFn(node, 'android:translateX', layer.translateX, 0); - conditionalAttrFn(node, 'android:translateY', layer.translateY, 0); - conditionalAttrFn(node, 'android:scaleX', layer.scaleX, 1); - conditionalAttrFn(node, 'android:scaleY', layer.scaleY, 1); - conditionalAttrFn(node, 'android:rotation', layer.rotation, 0); - parentNode.appendChild(node); - return node; - } - }, - destinationNode, - ); -} - -function conditionalAttrFn(node: any, attr: any, value: any, skipValue?: any) { - if (!_.isNil(value) && (skipValue === undefined || value !== skipValue)) { - node.setAttributeNS(ANDROID_NS, attr, value); - } -} - -function serializeXmlNode(xmlNode: any) { - return XmlSerializer.serializeToString(xmlNode, { indent: 4, multiAttributeIndent: 4 }); -} - -function walk(layer: VectorLayer, fn: any, context: any) { - const visitFn = (l: Layer, ctx: any) => { - const childCtx = fn(l, ctx); - if (l.children) { - l.children.forEach(child => visitFn(child, childCtx)); - } - }; - visitFn(layer, context); -} diff --git a/src/app/pages/editor/scripts/export/SpriteSerializer.ts b/src/app/pages/editor/scripts/export/SpriteSerializer.ts deleted file mode 100644 index 31c4e9ed..00000000 --- a/src/app/pages/editor/scripts/export/SpriteSerializer.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { VectorLayer } from 'app/pages/editor/model/layers'; -import { Animation } from 'app/pages/editor/model/timeline'; -import { AnimationRenderer } from 'app/pages/editor/scripts/animator'; -import { optimizeSvg } from 'app/pages/editor/scripts/svgo'; - -import * as SvgSerializer from './SvgSerializer'; - -export function createHtml(svgFileName: string, cssFileName: string) { - return ` - - - - -
- - -`; -} - -export function createCss(width: number, height: number, duration: number, numSteps: number) { - return ( - createKeyframes(width, numSteps) + - ` -.shapeshifter { - animation-duration: ${duration}ms; - animation-timing-function: steps(${numSteps}); - width: ${width}px; - height: ${height}px; - background-repeat: no-repeat; -} -.shapeshifter.play { - animation-name: play${numSteps}; -} -` - ); -} - -function createKeyframes(width: number, numSteps: number) { - return `@keyframes play${numSteps} { - 0% { - background-position: 0px 0px; - } - 100% { - background-position: -${numSteps * width}px 0px; - } -}`; -} - -export function createSvgFrames(vectorLayer: VectorLayer, animation: Animation, numSteps: number) { - const renderer = new AnimationRenderer(vectorLayer, animation); - const svgs: string[] = []; - const { width, height } = vectorLayer; - for (let i = 0; i <= numSteps; i++) { - const time = (i / numSteps) * animation.duration; - svgs.push(SvgSerializer.toSvgString(renderer.setCurrentTime(time), width, height)); - } - return svgs; -} - -export function createSvgSprite(vectorLayer: VectorLayer, animation: Animation, numSteps: number) { - const renderer = new AnimationRenderer(vectorLayer, animation); - const svgs: string[] = []; - const { width, height } = vectorLayer; - for (let i = 0; i <= numSteps; i++) { - const time = (i / numSteps) * animation.duration; - const vl = renderer.setCurrentTime(time); - svgs.push(SvgSerializer.toSvgSpriteFrameString(vl, width * i, 0, i.toString())); - } - const totalWidth = width * numSteps + width; - const svg = - ` -${svgs.join('\n')} - -`; - return optimizeSvg(svg, false); -} diff --git a/src/app/pages/editor/scripts/export/SvgSerializer.ts b/src/app/pages/editor/scripts/export/SvgSerializer.ts deleted file mode 100644 index 6e4ffd7a..00000000 --- a/src/app/pages/editor/scripts/export/SvgSerializer.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { - ClipPathLayer, - GroupLayer, - Layer, - LayerUtil, - PathLayer, - VectorLayer, -} from 'app/pages/editor/model/layers'; -import { ColorUtil } from 'app/pages/editor/scripts/common'; -import * as _ from 'lodash'; - -import * as XmlSerializer from './XmlSerializer'; - -const XMLNS_NS = 'http://www.w3.org/2000/xmlns/'; -const SVG_NS = 'http://www.w3.org/2000/svg'; - -/** - * Serializes an VectorLayer to a SVG string. - */ -export function toSvgString(vl: VectorLayer, width?: number, height?: number) { - const xmlDoc = document.implementation.createDocument(undefined, 'svg', undefined); - const rootNode = xmlDoc.documentElement; - rootNode.setAttributeNS(XMLNS_NS, 'xmlns', SVG_NS); - rootNode.setAttributeNS(undefined, 'viewBox', `0 0 ${vl.width} ${vl.height}`); - vectorLayerToSvgNode(vl, rootNode, xmlDoc); - if (width !== undefined) { - rootNode.setAttributeNS(undefined, 'width', width.toString() + 'px'); - } - if (height !== undefined) { - rootNode.setAttributeNS(undefined, 'height', height.toString() + 'px'); - } - return serializeXmlNode(rootNode); -} - -export function toSvgSpriteFrameString( - vectorLayer: VectorLayer, - translateX = 0, - translateY = 0, - frameNumber = '', -) { - const xmlDoc = document.implementation.createDocument(undefined, 'g', undefined); - const rootNode = xmlDoc.documentElement; - vectorLayerToSvgNode(vectorLayer, rootNode, xmlDoc, false, frameNumber); - rootNode.setAttributeNS(undefined, 'transform', `translate(${translateX}, ${translateY})`); - return serializeXmlNode(rootNode); -} - -/** - * Helper method that serializes a VectorLayer to a destinationNode in an xmlDoc. - * The destinationNode should be a node. - */ -function vectorLayerToSvgNode( - vl: VectorLayer, - destinationNode: HTMLElement, - xmlDoc: Document, - withIds = true, - frameNumber = '', -) { - // Create a map where the keys are ClipPathLayer IDs and the values - // are their associated path data strings. - const clipPathToPathDataMap = new Map(); - // Create a map where the keys are non-ClipPathLayer IDs and the values are - // the in-order list of ClipPathLayers that are clipping the layer (nearest - // ClipPathLayer appears in the list last). - const clippedLayerToSeenClipPathsMap = new Map>(); - (function recurseFn(layer: Layer) { - interface Entry { - readonly layer: Layer; - readonly seenClipPaths: ReadonlyArray; - } - layer.children - .reduce( - (acc: ReadonlyArray, curr) => { - const seenClipPaths = acc.length ? [..._.last(acc).seenClipPaths] : []; - // Ignore clip paths with empty path data strings. - if (curr instanceof ClipPathLayer && curr.pathData && curr.pathData.getPathString()) { - clipPathToPathDataMap.set(curr.id, curr.pathData.getPathString()); - seenClipPaths.push(curr); - } - return [...acc, { layer: curr, seenClipPaths }]; - }, - [] as ReadonlyArray, - ) - .filter(({ layer: l, seenClipPaths }) => { - // Keep the entry if the key isn't a ClipPathLayer and its - // associated list of seen clip paths isn't empty. - return !(l instanceof ClipPathLayer) && seenClipPaths.length > 0; - }) - .map(({ layer: l, seenClipPaths }) => { - return { layerId: l.id, seenClipPaths: seenClipPaths.map(({ id }) => id) }; - }) - .forEach(({ layerId, seenClipPaths }) => { - clippedLayerToSeenClipPathsMap.set(layerId, seenClipPaths); - }); - layer.children.forEach(recurseFn); - })(vl); - - // Create a map where the keys are non-ClipPathLayer IDs and the values are the - // clip path names they should use when referencing the clip-path. - const clippedLayerToClipPathNameMap = new Map(); - clippedLayerToSeenClipPathsMap.forEach((seenClipPaths, layerId) => { - const frameInfo = frameNumber ? `_frame${frameNumber}` : ''; - const layerInfo = `_${vl.findLayerById(layerId).name}`; - const clipPathName = `clip${frameInfo}${layerInfo}`; - clippedLayerToClipPathNameMap.set(layerId, clipPathName); - }); - - const shouldCreateDefs = clippedLayerToSeenClipPathsMap.size > 0; - if (shouldCreateDefs) { - const defsNode = xmlDoc.createElement('defs'); - clippedLayerToSeenClipPathsMap.forEach((seenClipPaths, layerId) => { - const clipPathName = clippedLayerToClipPathNameMap.get(layerId); - seenClipPaths.forEach((id, i) => { - const clipPathNode = xmlDoc.createElement('clipPath'); - conditionalAttr(clipPathNode, 'id', clipPathName + (i ? '_' + i : '')); - const pathNode = xmlDoc.createElement('path'); - conditionalAttr(pathNode, 'd', clipPathToPathDataMap.get(id)); - if (i + 1 < seenClipPaths.length) { - // Build the intersection of all seen clip paths. - const nextClipPathName = clipPathName + '_' + (i + 1); - conditionalAttr(pathNode, 'clip-path', `url(#${nextClipPathName})`); - } - clipPathNode.appendChild(pathNode); - defsNode.appendChild(clipPathNode); - }); - }); - destinationNode.appendChild(defsNode); - } - - const isLayerBeingClippedFn = (layerId: string) => clippedLayerToClipPathNameMap.has(layerId); - const maybeSetClipPathForLayerFn = (node: HTMLElement, layerId: string) => { - if (isLayerBeingClippedFn(layerId)) { - conditionalAttr(node, 'clip-path', `url(#${clippedLayerToClipPathNameMap.get(layerId)})`); - } - }; - - walk( - vl, - (layer: VectorLayer | GroupLayer | PathLayer, parentNode: Node) => { - if (layer instanceof VectorLayer) { - if (withIds) { - conditionalAttr(destinationNode, 'id', vl.name, ''); - } - conditionalAttr(destinationNode, 'opacity', vl.alpha, 1); - return parentNode; - } - if (layer instanceof PathLayer) { - const { pathData } = layer; - if (!pathData || !pathData.getPathString()) { - return undefined; - } - const node = xmlDoc.createElement('path'); - if (withIds) { - conditionalAttr(node, 'id', layer.name); - } - maybeSetClipPathForLayerFn(node, layer.id); - conditionalAttr(node, 'd', pathData.getPathString()); - if (layer.fillColor) { - conditionalAttr(node, 'fill', ColorUtil.androidToCssHexColor(layer.fillColor), ''); - } else { - conditionalAttr(node, 'fill', 'none'); - } - conditionalAttr(node, 'fill-opacity', layer.fillAlpha, 1); - if (layer.strokeColor) { - conditionalAttr(node, 'stroke', ColorUtil.androidToCssHexColor(layer.strokeColor), ''); - } - conditionalAttr(node, 'stroke-opacity', layer.strokeAlpha, 1); - conditionalAttr(node, 'stroke-width', layer.strokeWidth, 0); - - if (layer.trimPathStart !== 0 || layer.trimPathEnd !== 1 || layer.trimPathOffset !== 0) { - const flattenedTransform = LayerUtil.getCanvasTransformForLayer(vl, layer.id); - const { a, d } = flattenedTransform; - // Note that we only return the length of the first sub path due to - // https://code.google.com/p/android/issues/detail?id=172547 - let pathLength: number; - if (Math.abs(a) !== 1 || Math.abs(d) !== 1) { - // Then recompute the scaled path length. - pathLength = pathData - .mutate() - .transform(flattenedTransform) - .build() - .getSubPathLength(0); - } else { - pathLength = pathData.getSubPathLength(0); - } - const strokeDashArray = LayerUtil.toStrokeDashArray( - layer.trimPathStart, - layer.trimPathEnd, - layer.trimPathOffset, - pathLength, - ).join(','); - const strokeDashOffset = LayerUtil.toStrokeDashOffset( - layer.trimPathStart, - layer.trimPathEnd, - layer.trimPathOffset, - pathLength, - ).toString(); - conditionalAttr(node, 'stroke-dasharray', strokeDashArray); - conditionalAttr(node, 'stroke-dashoffset', strokeDashOffset); - } - - conditionalAttr(node, 'stroke-linecap', layer.strokeLinecap, 'butt'); - conditionalAttr(node, 'stroke-linejoin', layer.strokeLinejoin, 'miter'); - conditionalAttr(node, 'stroke-miterlimit', layer.strokeMiterLimit, 4); - const fillRule = !layer.fillType || layer.fillType === 'nonZero' ? 'nonzero' : 'evenodd'; - conditionalAttr(node, 'fill-rule', fillRule, 'nonzero'); - parentNode.appendChild(node); - return parentNode; - } - if (layer instanceof GroupLayer) { - const node = xmlDoc.createElement('g'); - if (withIds) { - conditionalAttr(node, 'id', layer.name); - } - const transformValues: string[] = []; - if (layer.translateX || layer.translateY) { - transformValues.push(`translate(${layer.translateX} ${layer.translateY})`); - } - if (layer.rotation) { - transformValues.push(`rotate(${layer.rotation} ${layer.pivotX} ${layer.pivotY})`); - } - if (layer.scaleX !== 1 || layer.scaleY !== 1) { - if (layer.pivotX || layer.pivotY) { - transformValues.push(`translate(${layer.pivotX} ${layer.pivotY})`); - } - transformValues.push(`scale(${layer.scaleX} ${layer.scaleY})`); - if (layer.pivotX || layer.pivotY) { - transformValues.push(`translate(${-layer.pivotX} ${-layer.pivotY})`); - } - } - let nodeToAttachToParent = node; - if (transformValues.length) { - node.setAttributeNS(undefined, 'transform', transformValues.join(' ')); - if (isLayerBeingClippedFn(layer.id)) { - // Create a wrapper node so that the clip-path is applied before the transformations. - const wrapperNode = xmlDoc.createElement('g'); - wrapperNode.appendChild(node); - nodeToAttachToParent = wrapperNode; - } - } - maybeSetClipPathForLayerFn(nodeToAttachToParent, layer.id); - parentNode.appendChild(nodeToAttachToParent); - return node; - } - return undefined; - }, - destinationNode, - ); -} - -function conditionalAttr( - node: HTMLElement, - attr: string, - value: string | number, - skipValue?: string | number, -) { - if (!_.isNil(value) && (skipValue === undefined || value !== skipValue)) { - node.setAttributeNS(undefined, attr, value.toString()); - } -} - -function serializeXmlNode(xmlNode: HTMLElement) { - return XmlSerializer.serializeToString(xmlNode, { indent: 4, multiAttributeIndent: 4 }); -} - -function walk(layer: VectorLayer, fn: (layer: Layer, ctx: Node) => Node, context: Node) { - const visitFn = (l: Layer, ctx: Node) => { - const childCtx = fn(l, ctx); - if (l.children) { - l.children.forEach(child => visitFn(child, childCtx)); - } - }; - visitFn(layer, context); -} diff --git a/src/app/pages/editor/scripts/export/XmlSerializer.ts b/src/app/pages/editor/scripts/export/XmlSerializer.ts deleted file mode 100644 index 3aab2f3e..00000000 --- a/src/app/pages/editor/scripts/export/XmlSerializer.ts +++ /dev/null @@ -1,143 +0,0 @@ -export function serializeToString(node: any, options: any): string { - options = options || {}; - options.rootNode = true; - return removeInvalidCharacters(nodeTreeToXHTML(node, options)); -} - -function removeInvalidCharacters(content: string) { - // See http://www.w3.org/TR/xml/#NT-Char for valid XML 1.0 characters. - return content.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, ''); -} - -function serializeAttributeValue(value: string) { - return value - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -function serializeTextContent(content: string) { - return content - .replace(/&/g, '&') - .replace(//g, '>'); -} - -function serializeAttribute(attr: any) { - const value = attr.value; - return attr.name + '="' + serializeAttributeValue(value) + '"'; -} - -function getTagName(node: Element) { - let tagName = node.tagName; - - // Aid in serializing of original HTML documents. - if (node.namespaceURI === 'http://www.w3.org/1999/xhtml') { - tagName = tagName.toLowerCase(); - } - return tagName; -} - -function serializeNamespace(node: any, options: any) { - const nodeHasXmlnsAttr = - Array.prototype.map - .call(node.attributes || node.attrs, (attr: any) => { - return attr.name; - }) - .indexOf('xmlns') >= 0; - - // Serialize the namespace as an xmlns attribute whenever the element - // doesn't already have one and the inherited namespace does not match - // the element's namespace. - if ( - !nodeHasXmlnsAttr && - node.namespaceURI && - options.isRootNode /* || - node.namespaceURI !== node.parentNode.namespaceURI*/ - ) { - return ' xmlns="' + node.namespaceURI + '"'; - } - return ''; -} - -function serializeChildren(node: Element, options: any) { - return Array.prototype.map - .call(node.childNodes, (childNode: any) => { - return nodeTreeToXHTML(childNode, options); - }) - .join(''); -} - -function serializeTag(node: any, options: any) { - let output = ''; - if (options.indent && options._indentLevel) { - output += Array(options._indentLevel * options.indent + 1).join(' '); - } - output += '<' + getTagName(node); - output += serializeNamespace(node, options.isRootNode); - - const attributes = node.attributes || node.attrs; - Array.prototype.forEach.call(attributes, (attr: any) => { - if (options.multiAttributeIndent && attributes.length > 1) { - output += '\n'; - output += Array( - (options._indentLevel || 0) * options.indent + options.multiAttributeIndent + 1, - ).join(' '); - } else { - output += ' '; - } - output += serializeAttribute(attr); - }); - - if (node.childNodes.length > 0) { - output += '>'; - if (options.indent) { - output += '\n'; - } - options.isRootNode = false; - options._indentLevel = (options._indentLevel || 0) + 1; - output += serializeChildren(node, options); - --options._indentLevel; - if (options.indent && options._indentLevel) { - output += Array(options._indentLevel * options.indent + 1).join(' '); - } - output += ''; - } else { - output += '/>'; - } - if (options.indent) { - output += '\n'; - } - return output; -} - -function serializeText(node: any) { - const text = node.nodeValue || node.value || ''; - return serializeTextContent(text); -} - -function serializeComment(node: any) { - return ''; -} - -function serializeCDATA(node: Element) { - return ''; -} - -function nodeTreeToXHTML(node: Element, options: any) { - if (node.nodeName === '#document' || node.nodeName === '#document-fragment') { - return serializeChildren(node, options); - } else { - if (node.tagName) { - return serializeTag(node, options); - } else if (node.nodeName === '#text') { - return serializeText(node); - } else if (node.nodeName === '#comment') { - return serializeComment(node); - } else if (node.nodeName === '#cdata-section') { - return serializeCDATA(node); - } - } -} diff --git a/src/app/pages/editor/scripts/export/index.ts b/src/app/pages/editor/scripts/export/index.ts deleted file mode 100644 index 2e526bfd..00000000 --- a/src/app/pages/editor/scripts/export/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as AvdSerializer from './AvdSerializer'; -import * as SpriteSerializer from './SpriteSerializer'; -import * as SvgSerializer from './SvgSerializer'; -export { AvdSerializer, SpriteSerializer, SvgSerializer }; diff --git a/src/app/pages/editor/scripts/import/SvgLoader.spec.ts b/src/app/pages/editor/scripts/import/SvgLoader.spec.ts deleted file mode 100644 index 17b72c4c..00000000 --- a/src/app/pages/editor/scripts/import/SvgLoader.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { PathLayer } from 'app/pages/editor/model/layers'; - -import { SvgLoader } from '.'; - -describe('SvgLoader', () => { - it(`can import simple SVG`, done => { - const svg = ` - - - -`; - SvgLoader.loadVectorLayerFromSvgString(svg, () => false).then(vl => { - expect(vl.width).toBe(24); - expect(vl.height).toBe(24); - expect(vl.children.length).toBe(1); - const pathLayer = vl.children[0] as PathLayer; - expect(pathLayer.name).toBe('path'); - expect(pathLayer.fillColor).toBe('#000'); - expect(pathLayer.pathData.getPathString()).toBe('M 0 0 L 10 10 L 20 20 L 30 30'); - done(); - }); - }); - - it(`can import simple SVG with viewBox translation`, done => { - const svg = ` - - - -`; - SvgLoader.loadVectorLayerFromSvgString(svg, () => false).then(vl => { - expect(vl.width).toBe(24); - expect(vl.height).toBe(24); - expect(vl.children.length).toBe(1); - const pathLayer = vl.children[0] as PathLayer; - expect(pathLayer.name).toBe('path'); - expect(pathLayer.fillColor).toBe('#000'); - expect(pathLayer.pathData.getPathString()).toBe('M -5 10 L 5 20 L 15 30 L 25 40'); - done(); - }); - }); - - it(`can import simple SVG with group/path transformations`, done => { - const svg = ` - - - - - - - - - - -`; - SvgLoader.loadVectorLayerFromSvgString(svg, () => false).then(vl => { - const paths = [ - 'M 0 0 L 20 20 L 40 40 L 60 60', - 'M 0 0 L -20 -20 L -40 -40 L -60 -60', - 'M 0 0 L -20 -20 L -40 -40 L -60 -60', - 'M 10 20 L -10 0 L -30 -20 L -50 -40', - 'M -20 -40 L -40 -60 L -60 -80 L -80 -100', - 'M 20 40 L 40 60 L 60 80 L 80 100', - ]; - const actualPath = (vl.children[0] as PathLayer).pathData.getPathString(); - expect(actualPath).toBe(paths.join(' ')); - done(); - }); - }); - - it(`can import simple SVG with clip paths`, done => { - const svg = ` - - - - - - - - - - - - - - - - - - - - -`; - SvgLoader.loadVectorLayerFromSvgString(svg, () => false).then(vl => { - // TODO: test stuff - expect(true).toBe(true); - done(); - }); - }); -}); diff --git a/src/app/pages/editor/scripts/import/SvgLoader.ts b/src/app/pages/editor/scripts/import/SvgLoader.ts deleted file mode 100644 index 61a95bfe..00000000 --- a/src/app/pages/editor/scripts/import/SvgLoader.ts +++ /dev/null @@ -1,376 +0,0 @@ -import { - ClipPathLayer, - FillType, - GroupLayer, - Layer, - LayerUtil, - PathLayer, - StrokeLineCap, - StrokeLineJoin, - VectorLayer, -} from 'app/pages/editor/model/layers'; -import { Path } from 'app/pages/editor/model/paths'; -import { NameProperty } from 'app/pages/editor/model/properties'; -import { ColorUtil, MathUtil, Matrix } from 'app/pages/editor/scripts/common'; -import { optimizeSvg } from 'app/pages/editor/scripts/svgo'; -import * as _ from 'lodash'; - -// TODO: trim ids/strings? -// TODO: check for invalid enum values - -/** - * Utility function that takes an SVG string as input and - * returns a VectorLayer model object. - */ -export function loadVectorLayerFromSvgString( - svgString: string, - doesNameExistFn: (name: string) => boolean, -) { - return optimizeSvg(svgString).then(optimizedSvgString => { - return new Promise((resolve, reject) => { - if (!optimizedSvgString) { - reject(); - return; - } - resolve(loadVectorLayerFromSvgStringInternal(optimizedSvgString, doesNameExistFn)); - }); - }); -} - -// TODO: give better error message when user attempts to import SVG w/o a namespace declaration -function loadVectorLayerFromSvgStringInternal( - svgString: string, - doesNameExistFn: (name: string) => boolean, -): VectorLayer { - const usedIds = new Set(); - const makeFinalNodeIdFn = (nodeId: string, prefix: string) => { - const finalName = LayerUtil.getUniqueName( - NameProperty.sanitize(nodeId || prefix), - name => doesNameExistFn(name) || usedIds.has(name), - ); - usedIds.add(finalName); - return finalName; - }; - - const parser = new DOMParser(); - const { documentElement } = parser.parseFromString(svgString, 'image/svg+xml'); - if (!isSvgNode(documentElement)) { - return undefined; - } - - // TODO: handle clipPaths that have children path elements with clip-path attributes - // TODO: handle clipPaths with clipPathUnits="objectBoundingBox" - // TODO: confirm that clipPath transforms (and any referenced transforms) are handled correctly - const clipPathMap = _.mapValues(buildPathInfosMap(documentElement), infos => { - return infos.map(info => info.path); - }); - - const nodeToLayerFn = (node: Element, transforms: ReadonlyArray): Layer => { - if ( - !node || - node.nodeType === Node.TEXT_NODE || - node.nodeType === Node.COMMENT_NODE || - node instanceof SVGDefsElement || - node instanceof SVGUseElement - ) { - return undefined; - } - - const nodeTransforms = getNodeTransforms(node as SVGGraphicsElement); - transforms = [...transforms, ...nodeTransforms]; - const flattenedTransforms = Matrix.flatten(transforms); - - // Get the referenced clip-path ID, if one exists. - const refClipPathId = getReferencedClipPathId(node); - - const maybeWrapClipPathInGroupFn = (layer: Layer) => { - if (!refClipPathId) { - return layer; - } - const paths = (clipPathMap[refClipPathId] || []).map(p => { - return new Path( - p - .mutate() - .transform(flattenedTransforms) - .build() - .getPathString(), - ); - }); - if (!paths.length) { - // If the clipPath has no children, then clip the entire layer. - paths.push(new Path('M 0 0 Z')); - } - const groupChildren: Layer[] = paths.map(p => { - return new ClipPathLayer({ - name: makeFinalNodeIdFn(refClipPathId, 'mask'), - pathData: p, - children: [], - }); - }); - groupChildren.push(layer); - return new GroupLayer({ - name: makeFinalNodeIdFn('wrapper', 'group'), - children: groupChildren, - }); - }; - - if (node instanceof SVGPathElement && node.getAttribute('d')) { - const path = node.getAttribute('d'); - const attrMap: Dictionary = {}; - const simpleAttrFn = (nodeAttr: string, contextAttr: string) => { - if (node.hasAttribute(nodeAttr)) { - attrMap[contextAttr] = node.getAttribute(nodeAttr); - } - }; - - simpleAttrFn('stroke', 'strokeColor'); - simpleAttrFn('stroke-width', 'strokeWidth'); - simpleAttrFn('stroke-linecap', 'strokeLinecap'); - simpleAttrFn('stroke-linejoin', 'strokeLinejoin'); - simpleAttrFn('stroke-miterlimit', 'strokeMiterLimit'); - simpleAttrFn('stroke-opacity', 'strokeAlpha'); - simpleAttrFn('fill', 'fillColor'); - simpleAttrFn('fill-opacity', 'fillAlpha'); - simpleAttrFn('fill-rule', 'fillType'); - - // Set the default values as specified by the SVG spec. Note that some of these default - // values are different than the default values used by VectorDrawables. - const fillColor = - 'fillColor' in attrMap ? ColorUtil.svgToAndroidColor(attrMap['fillColor']) : '#000'; - const strokeColor = - 'strokeColor' in attrMap ? ColorUtil.svgToAndroidColor(attrMap['strokeColor']) : undefined; - const fillAlpha = 'fillAlpha' in attrMap ? Number(attrMap['fillAlpha']) : 1; - let strokeWidth = 'strokeWidth' in attrMap ? Number(attrMap['strokeWidth']) : 1; - const strokeAlpha = 'strokeAlpha' in attrMap ? Number(attrMap['strokeAlpha']) : 1; - const strokeLinecap: StrokeLineCap = - 'strokeLinecap' in attrMap ? attrMap['strokeLinecap'] : 'butt'; - const strokeLinejoin: StrokeLineJoin = - 'strokeLinejoin' in attrMap ? attrMap['strokeLinecap'] : 'miter'; - const strokeMiterLimit = - 'strokeMiterLimit' in attrMap ? Number(attrMap['strokeMiterLimit']) : 4; - const fillRuleToFillTypeFn = (fillRule: string) => { - return fillRule === 'evenodd' ? 'evenOdd' : 'nonZero'; - }; - const fillType: FillType = - 'fillType' in attrMap ? fillRuleToFillTypeFn(attrMap['fillType']) : 'nonZero'; - - let pathData = new Path(path); - if (transforms.length) { - pathData = new Path( - pathData - .mutate() - .transform(flattenedTransforms) - .build() - .getPathString(), - ); - strokeWidth = MathUtil.round(strokeWidth * flattenedTransforms.getScaleFactor()); - } - // TODO: make best effort attempt to restore trimPath{Start,End,Offset} - return maybeWrapClipPathInGroupFn( - new PathLayer({ - id: _.uniqueId(), - name: makeFinalNodeIdFn(node.getAttribute('id'), 'path'), - children: [], - pathData, - fillColor, - fillAlpha, - strokeColor, - strokeAlpha, - strokeWidth, - strokeLinecap, - strokeLinejoin, - strokeMiterLimit, - fillType, - }), - ); - } - - // TODO: we should *not* iterate over a clip path's children here... - if (node.childNodes) { - const children: Layer[] = []; - for (let i = 0; i < node.childNodes.length; i++) { - const child = node.childNodes.item(i) as Element; - const layer = nodeToLayerFn(child, transforms); - if (layer) { - children.push(layer); - } - } - return maybeWrapClipPathInGroupFn( - new GroupLayer({ - id: _.uniqueId(), - name: makeFinalNodeIdFn(node.getAttribute('id'), 'group'), - children, - }), - ); - } - return undefined; - }; - - const toNumberFn = (num: any) => (num === undefined ? undefined : Number(num)); - let width = toNumberFn(svgLengthToPx(documentElement.width) || undefined); - let height = toNumberFn(svgLengthToPx(documentElement.height) || undefined); - const alpha = toNumberFn(documentElement.getAttribute('opacity') || undefined); - - const rootTransforms: Matrix[] = []; - const { viewBox } = documentElement; - if (viewBox && (!!viewBox.baseVal.width || !!viewBox.baseVal.height)) { - width = viewBox.baseVal.width; - height = viewBox.baseVal.height; - - // Fake a translate transform for the viewbox. - rootTransforms.push(Matrix.translation(-viewBox.baseVal.x, -viewBox.baseVal.y)); - } - const rootLayer = nodeToLayerFn(documentElement, rootTransforms); - return new VectorLayer({ - id: _.uniqueId(), - name: makeFinalNodeIdFn(documentElement.getAttribute('id'), 'vector'), - children: rootLayer ? rootLayer.children : undefined, - width, - height, - alpha, - }); -} - -function svgLengthToPx(svgLength: any) { - if (!svgLength) { - return 0; - } - if (svgLength.baseVal) { - svgLength = svgLength.baseVal; - } - svgLength.convertToSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX); - return svgLength.valueInSpecifiedUnits; -} - -function isSvgNode(node: Element): node is SVGSVGElement { - return node.nodeName === 'svg'; -} - -/** - * Returns a list of transform matricies assigned to the specified node. - */ -function getNodeTransforms(node: SVGGraphicsElement) { - if (!node.transform) { - return []; - } - const transformList = node.transform.baseVal; - const matrices: Matrix[] = []; - for (let i = 0; i < transformList.numberOfItems; i++) { - const { a, b, c, d, e, f } = transformList.getItem(i).matrix; - matrices.push(new Matrix(a, b, c, d, e, f)); - } - return matrices; -} - -/** - * Returns the name of the referenced ID assigned to the clip-path attribute, - * if one exists. - */ -function getReferencedClipPathId(node: Element) { - if (!node.getAttribute('clip-path')) { - return undefined; - } - const clipPathAttr = node.getAttribute('clip-path').trim(); - if (!clipPathAttr || !clipPathAttr.startsWith('url(#')) { - return undefined; - } - const endParenIndex = clipPathAttr.indexOf(')'); - if (endParenIndex !== clipPathAttr.length - 1) { - return undefined; - } - return clipPathAttr.slice('url(#'.length, endParenIndex); -} - -interface PathInfo { - readonly path: Path; - readonly refClipPathId?: string; -} - -interface ClipPathInfo { - readonly pathInfos: ReadonlyArray; - readonly refClipPathId?: string; -} - -/** - * Builds a map of clip path IDs to their corresponding clip path nodes. - */ -function buildClipPathIdMap(rootNode: Element) { - const clipPathIdMap: Dictionary = {}; - (function recurseFn(node: Element) { - if (node instanceof SVGClipPathElement) { - const clipPathId = node.getAttribute('id'); - if (clipPathId) { - clipPathIdMap[clipPathId] = node; - } - return; - } - if (node && node.childNodes) { - for (let i = 0; i < node.childNodes.length; i++) { - recurseFn(node.childNodes.item(i) as Element); - } - } - })(rootNode); - return clipPathIdMap; -} - -/** - * Builds a list of path info objects for the specified clip path element. - */ -function buildPathInfosForClipPath(node: SVGClipPathElement) { - // TODO: make sure that transforms from parent clip-paths aren't inherited... - const clipPathTransforms = getNodeTransforms(node).reverse(); - - const pathInfos: PathInfo[] = []; - if (node.childNodes) { - for (let i = 0; i < node.childNodes.length; i++) { - const childNode = node.childNodes.item(i) as Element; - if (childNode instanceof SVGPathElement && childNode.getAttribute('d')) { - const pathStr = childNode.getAttribute('d'); - const pathTransforms = getNodeTransforms(childNode).reverse(); - const transforms = [...pathTransforms, ...clipPathTransforms]; - const refClipPathId = getReferencedClipPathId(childNode); - pathInfos.push({ - refClipPathId, - path: new Path( - new Path(pathStr) - .mutate() - .transform(Matrix.flatten(transforms)) - .build() - .getPathString(), - ), - }); - } - } - } - return pathInfos; -} - -/** - * Builds a map of clip path IDs to their corresponding path info objects. - */ -function buildPathInfosMap(root: Element) { - const clipPathInfoMap = _.mapValues(buildClipPathIdMap(root), n => { - const pathInfos = buildPathInfosForClipPath(n); - const refClipPathId = getReferencedClipPathId(n); - return { pathInfos, refClipPathId } as ClipPathInfo; - }); - const pathInfosMap: Dictionary> = {}; - const recurseFn = (clipPathId: string) => { - if (pathInfosMap[clipPathId]) { - // Then the path infos have already been computed. - return; - } - const { pathInfos, refClipPathId } = clipPathInfoMap[clipPathId]; - if (!refClipPathId) { - // Then simply assign the path infos to the clip path id. - pathInfosMap[clipPathId] = pathInfos; - return; - } - // Then concatenate the clip path's path info objects with its - // referenced path info objects. - recurseFn(refClipPathId); - pathInfosMap[clipPathId] = [...pathInfos, ...pathInfosMap[refClipPathId]]; - }; - Object.keys(clipPathInfoMap).forEach(recurseFn); - return pathInfosMap; -} diff --git a/src/app/pages/editor/scripts/import/VectorDrawableLoader.spec.ts b/src/app/pages/editor/scripts/import/VectorDrawableLoader.spec.ts deleted file mode 100644 index c2000905..00000000 --- a/src/app/pages/editor/scripts/import/VectorDrawableLoader.spec.ts +++ /dev/null @@ -1,304 +0,0 @@ -/* tslint:disable */ - -// import * as VectorDrawableLoader from './VectorDrawableLoader'; - -// fdescribe('VectorDrawableLoader', () => { -// it(`Load animation`, () => { -// const xml = ` -// - -// -// -// -// -// -// -// -// -// -// -// -// -// -// - -// -// - -// -// -// -// -// -// - -// -// - -// -// -// -// -// -// -// -// - -// -// - -// -// -// -// -// -// -// -// - -// -// -// -// -// -// -// -// -// -// `; -// VectorDrawableLoader.loadAnimationFromXmlString(xml, 'anim', () => false); -// }); -// }); diff --git a/src/app/pages/editor/scripts/import/VectorDrawableLoader.ts b/src/app/pages/editor/scripts/import/VectorDrawableLoader.ts deleted file mode 100644 index bc00f15e..00000000 --- a/src/app/pages/editor/scripts/import/VectorDrawableLoader.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { - ClipPathLayer, - FillType, - GroupLayer, - Layer, - LayerUtil, - PathLayer, - StrokeLineCap, - StrokeLineJoin, - VectorLayer, -} from 'app/pages/editor/model/layers'; -import { Path } from 'app/pages/editor/model/paths'; -import { NameProperty } from 'app/pages/editor/model/properties'; -import { ColorUtil } from 'app/pages/editor/scripts/common'; -import * as _ from 'lodash'; - -// import { INTERPOLATORS } from 'app/pages/editor/model/interpolators'; -// import { AnimationBlock } from 'app/pages/editor/model/timeline'; - -export function loadVectorLayerFromXmlString( - xmlString: string, - doesLayerNameExistFn: (name: string) => boolean, -) { - const parser = new DOMParser(); - const doc = parser.parseFromString(xmlString, 'application/xml'); - return loadVectorLayerFromElement(doc.documentElement, doesLayerNameExistFn); -} - -function loadVectorLayerFromElement( - docEl: HTMLElement, - doesLayerNameExistFn: (name: string) => boolean, -) { - if (!docEl) { - return undefined; - } - const usedNames = new Set(); - const makeFinalNodeIdFn = (value: string, prefix: string) => { - const finalName = LayerUtil.getUniqueName( - NameProperty.sanitize(value || prefix), - n => doesLayerNameExistFn(n) || usedNames.has(n), - ); - usedNames.add(finalName); - return finalName; - }; - - const nodeToLayerDataFn = (node: Node): Layer => { - if (!isElement(node)) { - return undefined; - } - - if (node.tagName === 'path') { - return new PathLayer({ - id: _.uniqueId(), - name: makeFinalNodeIdFn(node.getAttribute('android:name'), 'path'), - children: [], - pathData: getPath(node), - fillColor: getColor(node, 'fillColor', ''), - fillAlpha: getNumber(node, 'fillAlpha', '1'), - strokeColor: getColor(node, 'strokeColor', ''), - strokeAlpha: getNumber(node, 'strokeAlpha', '1'), - strokeWidth: getNumber(node, 'strokeWidth', '0'), - strokeLinecap: get(node, 'strokeLineCap', 'butt') as StrokeLineCap, - strokeLinejoin: get(node, 'strokeLineJoin', 'miter') as StrokeLineJoin, - strokeMiterLimit: getNumber(node, 'strokeMiterLimit', '4'), - trimPathStart: getNumber(node, 'trimPathStart', '0'), - trimPathEnd: getNumber(node, 'trimPathEnd', '1'), - trimPathOffset: getNumber(node, 'trimPathOffset', '0'), - fillType: get(node, 'fillType', 'nonZero') as FillType, - }); - } - - if (node.tagName === 'clip-path') { - return new ClipPathLayer({ - id: _.uniqueId(), - name: makeFinalNodeIdFn(get(node, 'name', ''), 'clip-path'), - children: [], - pathData: getPath(node), - }); - } - - if (node.childNodes.length) { - const children = Array.from(node.childNodes) - .map(child => nodeToLayerDataFn(child)) - .filter(child => !!child); - if (children && children.length) { - return new GroupLayer({ - id: _.uniqueId(), - name: makeFinalNodeIdFn(get(node, 'name', ''), 'group'), - children, - pivotX: getNumber(node, 'pivotX', '0'), - pivotY: getNumber(node, 'pivotY', '0'), - rotation: getNumber(node, 'rotation', '0'), - scaleX: getNumber(node, 'scaleX', '1'), - scaleY: getNumber(node, 'scaleY', '1'), - translateX: getNumber(node, 'translateX', '0'), - translateY: getNumber(node, 'translateY', '0'), - }); - } - } - - return undefined; - }; - - const rootLayer = nodeToLayerDataFn(docEl); - const name = makeFinalNodeIdFn(get(docEl, 'name', ''), 'vector'); - usedNames.add(name); - const width = getNumber(docEl, 'viewportWidth', '24'); - const height = getNumber(docEl, 'viewportHeight', '24'); - const alpha = getNumber(docEl, 'alpha', '1'); - return new VectorLayer({ - id: _.uniqueId(), - name, - children: rootLayer ? rootLayer.children : [], - width, - height, - alpha, - }); -} - -// export function loadAnimationFromXmlString( -// xmlString: string, -// animationName: string, -// doesLayerNameExistFn: (name: string) => boolean) { - -// const parser = new DOMParser(); -// const avdNode = parser.parseFromString(xmlString, 'application/xml').documentElement; -// const vl = -// _(Array.from(avdNode.childNodes)) -// .filter(elem => { -// return isElement(elem) -// && elem.tagName === 'aapt:attr' -// && elem.hasAttribute('name') -// && elem.getAttribute('name') === 'android:drawable'; -// }) -// .map((elem: HTMLElement) => { -// return _(Array.from(elem.childNodes)) -// .filter(e => isElement(e) && e.tagName === 'vector') -// .map((e: HTMLElement) => loadVectorLayerFromElement(e, doesLayerNameExistFn)) -// .first(); -// }) -// .first(); -// if (!vl) { -// return undefined; -// } -// const blocks = -// _(Array.from(avdNode.childNodes)) -// .filter(e => { -// return isElement(e) -// && e.tagName === 'target' -// && !!e.getAttribute('android:name'); -// }) -// .flatMap((targetElem: HTMLElement) => { -// const targetName = targetElem.getAttribute('android:name'); -// const layerId = vl.findLayerByName(targetName).id; -// const animElem = -// _(Array.from(targetElem.childNodes)) -// .filter(elem => { -// return isElement(elem) -// && elem.tagName === 'aapt:attr' -// && elem.hasAttribute('name') -// && elem.getAttribute('name') === 'android:animation' -// && elem.childNodes.length; -// }) -// .flatMap((elem: HTMLElement) => Array.from(elem.childNodes)) -// .filter(e => isElement(e) && (e.tagName === 'set' || e.tagName === 'objectAnimator')) -// .map((e: HTMLElement) => { -// if (e.tagName === 'set') { -// // TODO: handle animator set case -// return undefined; -// } -// // Otherwise it is an object animator. -// return e; -// }) -// .first() as HTMLElement; -// const animationBlocks: AnimationBlock[] = []; -// const propertyName = get(animElem, 'propertyName'); -// const fromValue = get(animElem, 'valueFrom'); -// const toValue = get(animElem, 'valueTo'); -// // TODO: confirm difference between @android:anim and @android:interpolator -// // TODO: @android:interpolator/linear doesn't work -// const interpolatorRef = -// get(animElem, 'interpolator', '@android:anim/accelerate_decelerate_interpolator'); -// const interpolator = _.find(INTERPOLATORS, i => i.androidRef === interpolatorRef).value; -// const startTime = Number(get(animElem, 'startOffset')); -// const endTime = startTime + Number(get(animElem, 'duration')); -// if (get(animElem, 'valueType') === 'pathType' && propertyName === 'pathData') { -// animationBlocks.push(AnimationBlock.from({ -// layerId, -// propertyName, -// fromValue: new Path(fromValue), -// toValue: new Path(toValue), -// startTime, -// endTime, -// interpolator, -// type: 'path', -// })); -// } else if (propertyName === 'fillAlpha' || propertyName === 'translateX') { -// animationBlocks.push(AnimationBlock.from({ -// layerId, -// propertyName, -// fromValue: Number(fromValue), -// toValue: Number(toValue), -// startTime, -// endTime, -// interpolator, -// type: 'number', -// })); -// } -// // TODO: return a list of animation blocks here -// return animationBlocks; -// }) -// .value(); -// const avdTargetElements = avdChildElements.filter(e => e.tagName === 'target'); -// avdTargetElements.forEach(e => getTargetFn(e)); -// console.info(vl, avdTargetElements); -// return undefined; -// } - -function isElement(node: Node): node is HTMLElement { - return ( - node && - node.nodeType !== Node.TEXT_NODE && - node.nodeType !== Node.COMMENT_NODE && - _.isElement(node) - ); -} - -function get(obj: HTMLElement, attr: string, def = '') { - const androidAttr = `android:${attr}`; - return obj.hasAttribute(androidAttr) ? obj.getAttribute(androidAttr) : def; -} - -function getNumber(obj: HTMLElement, attr: string, def: string) { - const androidAttr = `android:${attr}`; - const num = Number(obj.hasAttribute(androidAttr) ? obj.getAttribute(androidAttr) : def); - return isFinite(num) ? num : Number(def); -} - -function getPath(obj: HTMLElement) { - const androidAttr = 'android:pathData'; - const pathData = obj.hasAttribute(androidAttr) ? obj.getAttribute(androidAttr) : ''; - try { - return new Path(pathData); - } catch (e) { - console.warn('Failed to import pathData: ', pathData); - return undefined; - } -} - -function getColor(obj: HTMLElement, attr: string, def = '') { - const androidAttr = `android:${attr}`; - const color = obj.hasAttribute(androidAttr) ? obj.getAttribute(androidAttr) : def; - return !!ColorUtil.parseAndroidColor(color) ? color : def; -} diff --git a/src/app/pages/editor/scripts/import/index.ts b/src/app/pages/editor/scripts/import/index.ts deleted file mode 100644 index 39793fbc..00000000 --- a/src/app/pages/editor/scripts/import/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as SvgLoader from './SvgLoader'; -import * as VectorDrawableLoader from './VectorDrawableLoader'; -export { SvgLoader, VectorDrawableLoader }; diff --git a/src/app/pages/editor/scripts/intervals/IntervalTree.ts b/src/app/pages/editor/scripts/intervals/IntervalTree.ts deleted file mode 100644 index 6b14864c..00000000 --- a/src/app/pages/editor/scripts/intervals/IntervalTree.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * A naive implementation of an interval tree with O(n) search performance. - */ -export class IntervalTree { - private readonly intervals: Interval[] = []; - - /** - * Note that inserted numbers are automatically rounded to the nearest integer. - */ - insert(low: number, high: number, data: T) { - low = Math.round(low); - high = Math.round(high); - this.intervals.push({ low, high, data }); - } - - /** - * Check if the interval (low, high) intersects with any intervals in the tree. - * An extra predicateFn can be supplied to further filter the results. - */ - intersectsWith(low: number, high: number, predicateFn = (data: T) => true) { - return this.intervals.some(interval => { - return low < interval.high && interval.low < high && predicateFn(interval.data); - }); - } -} - -interface Interval { - readonly low: number; - readonly high: number; - readonly data: T; -} diff --git a/src/app/pages/editor/scripts/intervals/index.ts b/src/app/pages/editor/scripts/intervals/index.ts deleted file mode 100644 index 6307595f..00000000 --- a/src/app/pages/editor/scripts/intervals/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { IntervalTree } from './IntervalTree'; diff --git a/src/app/pages/editor/scripts/mixins/DestroyableMixin.ts b/src/app/pages/editor/scripts/mixins/DestroyableMixin.ts deleted file mode 100644 index 8c5c2de1..00000000 --- a/src/app/pages/editor/scripts/mixins/DestroyableMixin.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { OnDestroy } from '@angular/core'; -import { Subscription } from 'rxjs'; - -export function DestroyableMixin(Base = class {} as T) { - return class extends Base implements OnDestroy { - private readonly subscriptions: Subscription[] = []; - - protected registerSubscription(sub: Subscription) { - this.subscriptions.push(sub); - } - - ngOnDestroy() { - this.subscriptions.forEach(x => x.unsubscribe()); - } - }; -} diff --git a/src/app/pages/editor/scripts/mixins/index.ts b/src/app/pages/editor/scripts/mixins/index.ts deleted file mode 100644 index 01c5eb32..00000000 --- a/src/app/pages/editor/scripts/mixins/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DestroyableMixin } from './DestroyableMixin'; diff --git a/src/app/pages/editor/scripts/paper/PaperProject.ts b/src/app/pages/editor/scripts/paper/PaperProject.ts deleted file mode 100644 index 34d81397..00000000 --- a/src/app/pages/editor/scripts/paper/PaperProject.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { MasterToolPicker } from 'app/pages/editor/scripts/paper/tool'; -import { PaperService } from 'app/pages/editor/services'; -import { State, Store } from 'app/pages/editor/store'; -import { getHiddenLayerIds, getSelectedLayerIds } from 'app/pages/editor/store/layers/selectors'; -import { - getCreatePathInfo, - getEditPathInfo, - getHoveredLayerId, - getRotateItemsInfo, - getSelectionBox, - getSnapGuideInfo, - getSplitCurveInfo, - getToolMode, - getTooltipInfo, - getZoomPanInfo, -} from 'app/pages/editor/store/paper/selectors'; -import { getAnimatedVectorLayer } from 'app/pages/editor/store/playback/selectors'; -import * as paper from 'paper'; -import { Subscription } from 'rxjs'; - -export class PaperProject extends paper.Project { - private readonly paperLayer: PaperLayer; - private readonly masterToolPicker: MasterToolPicker; - private readonly subscriptions: Subscription[] = []; - - constructor(canvas: HTMLCanvasElement, ps: PaperService, store: Store) { - super(canvas); - const pl = new PaperLayer(ps); - paper.project.addLayer(pl); - this.paperLayer = pl; - this.masterToolPicker = new MasterToolPicker(ps); - this.subscriptions.push( - store.select(getToolMode).subscribe(() => this.masterToolPicker.onToolModeChanged()), - // TODO: dont allow the user to modify the vector layer when current time > 0 - store.select(getAnimatedVectorLayer).subscribe(() => pl.onVectorLayerChanged()), - store.select(getSelectedLayerIds).subscribe(() => pl.onSelectedLayerIdsChanged()), - store.select(getHoveredLayerId).subscribe(() => pl.onHoveredLayerIdChanged()), - store.select(getHiddenLayerIds).subscribe(() => pl.onHiddenLayerIdsChanged()), - store.select(getCreatePathInfo).subscribe(info => pl.setCreatePathInfo(info)), - store.select(getSplitCurveInfo).subscribe(info => pl.setSplitCurveInfo(info)), - store.select(getEditPathInfo).subscribe(info => pl.onEditPathInfoChanged()), - store.select(getRotateItemsInfo).subscribe(info => pl.onRotateItemsInfoChanged()), - store.select(getSnapGuideInfo).subscribe(info => pl.setSnapGuideInfo(info)), - store.select(getTooltipInfo).subscribe(info => pl.setTooltipInfo(info)), - store.select(getSelectionBox).subscribe(box => { - if (box) { - const from = new paper.Point(box.from); - const to = new paper.Point(box.to); - pl.setSelectionBox({ from, to }); - } else { - pl.setSelectionBox(undefined); - } - }), - store.select(getZoomPanInfo).subscribe(({ zoom, translation: { tx, ty } }) => { - this.view.matrix = new paper.Matrix(zoom, 0, 0, zoom, tx, ty); - }), - ); - } - - /** - * Sets the project's dimensions with the new VectorLayer viewport and canvas element size (in CSS pixels). - */ - setDimensions( - viewportWidth: number, - viewportHeight: number, - viewWidth: number, - viewHeight: number, - ) { - // The view size represents the actual size of the canvas in CSS pixels. - // The viewport size represents the user-visible dimensions (i.e. the default 24x24). - this.view.viewSize = new paper.Size(viewWidth, viewHeight); - this.paperLayer.setDimensions(viewportWidth, viewportHeight, viewWidth, viewHeight); - } - - // @Override - remove() { - super.remove(); - while (this.subscriptions.length) { - this.subscriptions.pop().unsubscribe(); - } - } -} diff --git a/src/app/pages/editor/scripts/paper/README.md b/src/app/pages/editor/scripts/paper/README.md deleted file mode 100644 index fd66caca..00000000 --- a/src/app/pages/editor/scripts/paper/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# paper.js notes - -This `README.md` file contains notes about the trickier aspects of using the amazing `paper.js` library. -It also contains some implementation details specific to Shape Shifter that are important to remember. - -## Setup - -Say we want to create a `paper.js` `Project` with the following canvas: - -```html - -``` - -In order to achieve this, we give our `Project` a `View` with the given size in CSS pixels (the canvas' -physical size will be assigned automatically, assuming `window.devicePixelRatio` is `2`): - -```js -paper.project.view.viewSize = new paper.Size(600, 600); -``` - -Say we want to display a vector graphic with a viewport width/height of `24`. Then we create a root -`PaperLayer` for the `Project`: - -```js -// 600 / 24 = 25 CSS pixels per viewport pixel. -const cssScaling = 600 / 24; - -const layer = new PaperLayer(); -layer.matrix = new paper.Matrix().scale(cssScaling); -paper.project.addLayer(layer); -``` - -`PaperLayer` is a special class that draws the vector graphic to the canvas, as well as a bunch of -other stuff, such as selection bounds, handles, segments, etc. - -## Coordinate spaces - -One of the trickiest aspects of using the library is dealing with different coordinate spaces. - -### Useful attributes/methods - -#### `Point` - -* `transform(m: paper.Matrix): paper.Point` - Transforms the point by the matrix as a new point. - -#### `Matrix` - -* `transform(p: paper.Point): paper.Point` - Transforms a point and returns the result. -* `inverseTransform(p: paper.Point): paper.Point` - Inverse transforms a point and returns the result. -* `appended(m: paper.Matrix): paper.Matrix` - Returns a new matrix as the result of appending the - specified matrix to this matrix. This is the equivalent of multiplying `(this matrix) * (specified matrix)`. -* `prepended(m: paper.Matrix): paper.Matrix` - Returns a new matrix as the result of prepending the - specified matrix to this matrix. This is the equivalent of multiplying `(specified matrix) s * (this matrix)`. - -#### `Item` - -* `globalMatrix: paper.Matrix` - The item's global transformation matrix in relation to the global - project coordinate space. Note that the view's transformations resulting from zooming and - panning are not factored in. -* `viewMatrix: paper.Matrix` - The item's global matrix in relation to the view coordinate space. - This means that the view's transformations resulting from zooming and panning are factored in. -* `globalToLocal(p: paper.Point): paper.Point` - Converts the specified point from global - project coordinate space to the item's own local coordinate space. -* `localToGlobal(p: paper.Point): paper.Point` - Converts the specified point from the - item's own local coordinate space to the global project coordinate space. -* `parentToLocal(p: paper.Point): paper.Point` - Converts the specified point from the - parent's coordinate space to item's own local coordinate space. -* `localToParent(p: paper.Point): paper.Point` - Converts the specified point from the - item's own local coordinate space to the parent's coordinate space. - -#### `View` - -* `projectToView(p: paper.Point): paper.Point` - Converts the passed point from project - coordinate space to view coordinate space, which is measured in browser pixels in relation - to the position of the view element. -* `viewToProject(p: paper.Point): paper.Point` - Converts the passed point from view - coordinate space to project coordinate space. - -### Project coordinates - -* Project coordinates are in terms of the canvas' size in CSS pixels (note that the - view's transformations resulting from zooming and panning are not factored in). -* Most of the `paper.js` API uses project coordinates (i.e. hit tests, mouse events, etc.). -* Project coordinate points are prefixed with `proj`. - -### Physical coordinates - -* Physical coordinates are in terms of the canvas' size in physical pixels. -* Physical coordinate points are prefixed with `phys`. -* To convert from project coordinates to physical coordinates, we can do: - -```js -// Same as window.devicePixelRatio. -const pixelRatio = paper.project.view.pixelRatio; -const physPoint = projPoint.transform(new Matrix().scale(pixelRatio)); -``` - -### View coordinates - -* View coordinates are project coordinates with zooming/panning factored in. -* View coordinate points are prefixed with `view`. -* To convert from project coordinates to view coordinates, we can do: - -```js -const viewPoint = paper.project.view.projectToView(projPoint); -``` - -### Viewport coordinates - -* Viewport coordinate points are prefixed with `vp`. -* Note that points that are saved to the store should always be in terms of - viewport coordinates. -* To convert from project coordinates to viewport coordinates, we can do: - -```js -const vpPoint = paperLayer.globalToLocal(projPoint); - -// ...which is the same as... - -const vpPoint = new Matrix().scale(cssScaling).inverseTransform(projPoint); -``` - -### Local coordinates - -* Local coordinate points are prefixed with `local`. - -### Parent coordinates - -* Parent coordinate points are prefixed with `parent`. - -## Tools diff --git a/src/app/pages/editor/scripts/paper/detector/ClickDetector.ts b/src/app/pages/editor/scripts/paper/detector/ClickDetector.ts deleted file mode 100644 index 0c5c5d6f..00000000 --- a/src/app/pages/editor/scripts/paper/detector/ClickDetector.ts +++ /dev/null @@ -1,85 +0,0 @@ -import * as paper from 'paper'; - -import { Handler } from './Handler'; - -const DOUBLE_CLICK_MIN_TIME = 40; -const DOUBLE_CLICK_TIMEOUT = 300; - -/** - * Helper class for detecting single/double click events. - */ -export class ClickDetector { - private readonly handler = new Handler(); - private currentDownEvent: paper.ToolEvent; - private previousUpEvent: paper.ToolEvent; - private isDoubleClicking = false; - private deferSingleClick = false; - private stillDown = false; - - onToolEvent(event: paper.ToolEvent) { - if (event.type === 'mousedown') { - this.processMouseDown(event); - } else if (event.type === 'mouseup') { - this.processMouseUp(event); - } - } - - private processMouseDown(event: paper.ToolEvent) { - const hadClickMessage = this.handler.hasPendingMessages(); - if (hadClickMessage) { - this.handler.removePendingMessages(); - } - const isDoubleClickFn = (firstUp: paper.ToolEvent, secondDown: paper.ToolEvent) => { - const deltaTime = secondDown.timeStamp - firstUp.timeStamp; - return DOUBLE_CLICK_MIN_TIME <= deltaTime && deltaTime <= DOUBLE_CLICK_TIMEOUT; - }; - if ( - this.currentDownEvent && - this.previousUpEvent && - hadClickMessage && - isDoubleClickFn(this.previousUpEvent, event) - ) { - // This is a second tap, so give a callback with the - // first click of the double click. - this.isDoubleClicking = true; - this.onDoubleClick(this.currentDownEvent); - } else { - // This is the first click. - this.handler.postDelayed(() => { - if (this.stillDown) { - // If the user's mouse is still down, do not dispatch the click - // event until the next mouse up event. - this.deferSingleClick = true; - } else { - // At this point we are certain that a second click is not coming, - // so dispatch the single click event. - this.onSingleClickConfirmed(event); - } - }, DOUBLE_CLICK_TIMEOUT); - } - this.currentDownEvent = event; - this.stillDown = true; - this.deferSingleClick = false; - } - - private processMouseUp(event: paper.ToolEvent) { - this.onSingleClick(event); - if (this.deferSingleClick) { - this.onSingleClickConfirmed(event); - } - this.stillDown = false; - this.previousUpEvent = event; - this.isDoubleClicking = false; - this.deferSingleClick = false; - } - - isDoubleClick() { - return this.isDoubleClicking; - } - - protected onSingleClick(event: paper.ToolEvent) {} - - protected onSingleClickConfirmed(event: paper.ToolEvent) {} - - protected onDoubleClick(event: paper.ToolEvent) {} -} diff --git a/src/app/pages/editor/scripts/paper/detector/Handler.ts b/src/app/pages/editor/scripts/paper/detector/Handler.ts deleted file mode 100644 index a3ac2a7b..00000000 --- a/src/app/pages/editor/scripts/paper/detector/Handler.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * A Handler implements a simple asynchronous message queue. Useful for - * actions that are inherently asynchronous, such as click events and - * other gestures. - */ -export class Handler { - private readonly pendingMessageIds = new Set(); - - postDelayed(fn: () => void, delayMillis: number) { - const id = window.setTimeout(() => { - this.pendingMessageIds.delete(id); - fn(); - }, Math.max(0, delayMillis)); - this.pendingMessageIds.add(id); - } - - hasPendingMessages() { - return this.pendingMessageIds.size > 0; - } - - removePendingMessages() { - this.pendingMessageIds.forEach(id => window.clearTimeout(id)); - this.pendingMessageIds.clear(); - } -} diff --git a/src/app/pages/editor/scripts/paper/detector/index.ts b/src/app/pages/editor/scripts/paper/detector/index.ts deleted file mode 100644 index fdf60161..00000000 --- a/src/app/pages/editor/scripts/paper/detector/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ClickDetector } from './ClickDetector'; diff --git a/src/app/pages/editor/scripts/paper/gesture/Gesture.ts b/src/app/pages/editor/scripts/paper/gesture/Gesture.ts deleted file mode 100644 index 7a4745be..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/Gesture.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as paper from 'paper'; - -/** - * A gesture represents a user interaction with the mouse or keyboard. Typically - * a gesture is used in one of two ways: - * - * (1) To monitor the state of events that occurs between the initial - * mouse down through the final mouse up. - * - * (2) To monitor mouse move events and react accordingly. - */ -export abstract class Gesture { - onMouseDown(event: paper.ToolEvent) {} - onMouseDrag(event: paper.ToolEvent) {} - onMouseMove(event: paper.ToolEvent) {} - onMouseUp(event: paper.ToolEvent) {} - onKeyDown(event: paper.KeyEvent) {} - onKeyUp(event: paper.KeyEvent) {} -} diff --git a/src/app/pages/editor/scripts/paper/gesture/create/EllipseGesture.ts b/src/app/pages/editor/scripts/paper/gesture/create/EllipseGesture.ts deleted file mode 100644 index 7f17972c..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/create/EllipseGesture.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as paper from 'paper'; - -import { ShapeGesture } from './ShapeGesture'; - -/** A gesture that creates an elliptical path. */ -export class EllipseGesture extends ShapeGesture { - // @Override - protected newPath(vpBounds: paper.Rectangle) { - return new paper.Path.Ellipse(vpBounds); - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/create/PencilGesture.ts b/src/app/pages/editor/scripts/paper/gesture/create/PencilGesture.ts deleted file mode 100644 index 710d9d3e..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/create/PencilGesture.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { ToolMode } from 'app/pages/editor/model/paper'; -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -/** - * A gesture that draws a path. - * - * Preconditions: - * - The user is in pencil mode. - */ -export class PencilGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - - // The last drag point in viewport coordinates. - private vpLastPoint: paper.Point; - - constructor(private readonly ps: PaperService) { - super(); - } - - // @Override - onMouseDrag(event: paper.ToolEvent) { - const vpDownPoint = this.pl.globalToLocal(event.downPoint); - const vpMiddlePoint = this.pl.globalToLocal(event.middlePoint); - const vpPoint = this.pl.globalToLocal(event.point); - if (!this.vpLastPoint) { - this.vpLastPoint = vpDownPoint; - } - const vpDelta = vpPoint.subtract(this.vpLastPoint); - vpDelta.angle += 90; - const createPathInfo = this.ps.getCreatePathInfo(); - const pencilPath = createPathInfo ? new paper.Path(createPathInfo.pathData) : new paper.Path(); - pencilPath.add(vpMiddlePoint.add(vpDelta)); - this.ps.setCreatePathInfo({ pathData: pencilPath.pathData, strokeColor: '#979797' }); - this.vpLastPoint = vpPoint; - } - - // @Override - onMouseUp(event: paper.ToolEvent) { - if (this.vpLastPoint) { - this.vpLastPoint = this.pl.globalToLocal(event.point); - } - this.finishGesture(); - } - - // @Override - onKeyDown(event: paper.KeyEvent) { - if (event.key === 'escape') { - this.finishGesture(); - } - } - - private finishGesture() { - if (this.vpLastPoint) { - const newPath = new paper.Path(this.ps.getCreatePathInfo().pathData); - const projStartPoint = this.pl.localToGlobal(newPath.firstSegment.point); - const projLastPoint = this.pl.localToGlobal(this.vpLastPoint); - // If the pencil path's start and end point are within 10px of each other - // at the end of the gesture, then we should close the path before saving - // it to the store. - if (projStartPoint.isClose(projLastPoint, 10)) { - newPath.closePath(true); - } - newPath.smooth({ type: 'continuous' }); - const newPathLayer = PaperUtil.addPathToStore(this.ps, newPath.pathData); - this.ps.setSelectedLayerIds(new Set([newPathLayer.id])); - this.ps.setCreatePathInfo(undefined); - } - this.ps.setToolMode(ToolMode.Default); - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/create/RectangleGesture.ts b/src/app/pages/editor/scripts/paper/gesture/create/RectangleGesture.ts deleted file mode 100644 index e8642b15..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/create/RectangleGesture.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as paper from 'paper'; - -import { ShapeGesture } from './ShapeGesture'; - -/** A gesture that creates a rectangular path. */ -export class RectangleGesture extends ShapeGesture { - // @Override - protected newPath(vpBounds: paper.Rectangle) { - return new paper.Path.Rectangle(vpBounds); - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/create/ShapeGesture.ts b/src/app/pages/editor/scripts/paper/gesture/create/ShapeGesture.ts deleted file mode 100644 index e1869a7f..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/create/ShapeGesture.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { ToolMode } from 'app/pages/editor/model/paper'; -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -/** Base class for all shape-building gestures. */ -export abstract class ShapeGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - - private vpLastDragInfo: Readonly<{ vpDownPoint: paper.Point; vpPoint: paper.Point }>; - - constructor(private readonly ps: PaperService) { - super(); - } - - // @Override - onMouseDrag(event: paper.ToolEvent) { - const vpDownPoint = this.pl.globalToLocal(event.downPoint); - const vpPoint = this.pl.globalToLocal(event.point); - this.vpLastDragInfo = { vpDownPoint, vpPoint }; - this.processEvent(event); - } - - // @Override - onMouseUp(event: paper.ToolEvent) { - if (this.vpLastDragInfo) { - const { pathData } = this.ps.getCreatePathInfo(); - const newPathLayer = PaperUtil.addPathToStore(this.ps, pathData); - this.ps.setSelectedLayerIds(new Set([newPathLayer.id])); - } - this.finishGesture(); - } - - // @Override - onKeyDown(event: paper.KeyEvent) { - if (event.key === 'shift' || event.key === 'alt') { - this.processEvent(event); - } else if (event.key === 'escape') { - this.finishGesture(); - } - } - - // @Override - onKeyUp(event: paper.KeyEvent) { - if (event.key === 'shift' || event.key === 'alt') { - this.processEvent(event); - } - } - - private processEvent({ modifiers: { alt, shift } }: paper.Event) { - const { vpDownPoint, vpPoint } = this.vpLastDragInfo; - - // If shift is pressed, then set the height equal to the width. - const vpSize = new paper.Size( - vpPoint.x - vpDownPoint.x, - shift ? vpPoint.x - vpDownPoint.x : vpPoint.y - vpDownPoint.y, - ).multiply(alt ? 2 : 1); - - // If alt is pressed, then the initial downpoint represents the shape's center point. - const vpTopLeft = alt - ? vpDownPoint.subtract(new paper.Point(vpSize.width / 2, vpSize.height / 2)) - : vpDownPoint; - - const { pathData } = this.newPath(new paper.Rectangle(vpTopLeft, vpSize)); - this.ps.setCreatePathInfo({ pathData, strokeColor: '#979797' }); - } - - private finishGesture() { - this.ps.setCreatePathInfo(undefined); - this.ps.setToolMode(ToolMode.Default); - } - - /** Factory method that creates a new path given its bounding box. */ - protected abstract newPath(vpBounds: paper.Rectangle): paper.Path; -} diff --git a/src/app/pages/editor/scripts/paper/gesture/create/index.ts b/src/app/pages/editor/scripts/paper/gesture/create/index.ts deleted file mode 100644 index 0ab8821a..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/create/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { RectangleGesture } from './RectangleGesture'; -export { EllipseGesture } from './EllipseGesture'; -export { PencilGesture } from './PencilGesture'; diff --git a/src/app/pages/editor/scripts/paper/gesture/edit/BatchSelectSegmentsGesture.ts b/src/app/pages/editor/scripts/paper/gesture/edit/BatchSelectSegmentsGesture.ts deleted file mode 100644 index 972868e0..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/edit/BatchSelectSegmentsGesture.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import * as _ from 'lodash'; -import * as paper from 'paper'; - -/** - * A gesture that selects multiple segments using a bounded box. - * - * Preconditions: - * - The user is in edit path mode. - */ -export class BatchSelectSegmentsGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - private initialSelectedSegments: ReadonlySet; - private updatedSelectedSegments: ReadonlySet; - private isDragging = false; - - constructor( - private readonly ps: PaperService, - private readonly editPathId: string, - private readonly clearEditPathAfterDraglessClick: boolean, - ) { - super(); - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - this.initialSelectedSegments = this.ps.getEditPathInfo().selectedSegments; - this.updatedSelectedSegments = new Set(); - this.updateCurrentSelection(event.modifiers.command || event.modifiers.shift); - } - - // @Override - onMouseDrag(event: paper.ToolEvent) { - this.isDragging = true; - this.ps.setSelectionBox({ - from: this.pl.globalToLocal(event.downPoint), - to: this.pl.globalToLocal(event.point), - }); - this.processToolEvent(event); - } - - // @Override - onMouseUp(event: paper.ToolEvent) { - if (this.isDragging) { - this.processToolEvent(event); - } else if (this.clearEditPathAfterDraglessClick) { - this.ps.setEditPathInfo(undefined); - } - this.ps.setSelectionBox(undefined); - } - - private processToolEvent(event: paper.ToolEvent) { - // Calculate the bounding rectangle to use to select segments in - // the edit path's local coordinate space. - const editPath = this.pl.findItemByLayerId(this.editPathId) as paper.Path; - const rectangle = new paper.Rectangle( - editPath.globalToLocal(event.downPoint), - editPath.globalToLocal(event.point), - ); - this.updatedSelectedSegments = new Set( - _.flatMap(editPath.segments, ({ point }, segmentIndex) => { - return rectangle.contains(point) ? [segmentIndex] : []; - }), - ); - this.updateCurrentSelection(event.modifiers.command || event.modifiers.shift); - } - - // @Override - onKeyDown(event: paper.KeyEvent) { - this.processKeyEvent(event); - } - - // @Override - onKeyUp(event: paper.KeyEvent) { - this.processKeyEvent(event); - } - - private processKeyEvent(event: paper.KeyEvent) { - if (event.key === 'ctrl' || event.key === 'meta' || event.key === 'shift') { - this.updateCurrentSelection(event.modifiers.command || event.modifiers.shift); - } - } - - private updateCurrentSelection(toggleInitialSelections: boolean) { - const selectedSegments = new Set(this.updatedSelectedSegments); - if (toggleInitialSelections) { - this.initialSelectedSegments.forEach(segmentIndex => { - if (selectedSegments.has(segmentIndex)) { - selectedSegments.delete(segmentIndex); - } else { - selectedSegments.add(segmentIndex); - } - }); - } - const editPath = this.pl.findItemByLayerId(this.editPathId) as paper.Path; - this.ps.setEditPathInfo({ ...PaperUtil.selectCurves(editPath, selectedSegments) }); - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/edit/MouldCurveGesture.ts b/src/app/pages/editor/scripts/paper/gesture/edit/MouldCurveGesture.ts deleted file mode 100644 index e9524236..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/edit/MouldCurveGesture.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -/** - * A gesture that allows the user to mould a curve by dragging a point on its path. - * - * Based on math from here: https://pomax.github.io/bezierinfo/#moulding - * - * Preconditions: - * - The user is in edit path mode. - * - The user hit one of the edit path's curves. - */ -export class MouldCurveGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - - private points: CubicPoints; - private B: paper.Point; - private C: paper.Point; - private ratio: number; - private t: number; - - // TODO: update HoverSegmentsCurvesGesture to *not* display a split path dot when command is held down - // TODO: handle cases where t === 0 and t === 1? - constructor( - private readonly ps: PaperService, - private readonly editPathId: string, - private readonly hitCurveInfo: Readonly<{ curveIndex: number; time: number }>, - ) { - super(); - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - const editPath = this.pl.findItemByLayerId(this.editPathId) as paper.Path; - const curve = editPath.curves[this.hitCurveInfo.curveIndex]; - const start = curve.segment1.point; - const end = curve.segment2.point; - const cp1 = start.add(curve.handle1); - const cp2 = end.add(curve.handle2); - const points: CubicPoints = [start, cp1, cp2, end]; - - const t = this.hitCurveInfo.time; - const A = hull(points, t)[5]; - const B = curve.getPointAtTime(t); - const C = lli(A, B, start, end); - const bottom = t ** 3 + (1 - t) ** 3; - const top = bottom - 1; - const ratio = Math.abs(top / bottom); - - // Cache these for later use. - this.points = points; - this.B = B; - this.C = C; - this.ratio = ratio; - this.t = t; - } - - // @Override - onMouseDrag(event: paper.ToolEvent) { - const editPath = this.pl.findItemByLayerId(this.editPathId) as paper.Path; - const localDownPoint = editPath.globalToLocal(event.downPoint); - const localDragPoint = editPath.globalToLocal(event.point); - - const { points, B, C, ratio, t } = this; - const newB = B.add(localDragPoint.subtract(localDownPoint)); - - // Preserve struts for B when repositioning. - const hullPoints = hull(this.points, t); - const Bl = hullPoints[7]; - const Br = hullPoints[8]; - const dbl = Bl.subtract(B); - const dbr = Br.subtract(B); - // Now that we know A, B, C and the AB:BC ratio, we can compute the new A' based on the desired B'. - const newA = newB.subtract(C.subtract(newB).divide(ratio)); - // Find new point on s--c1. - const p1 = newB.add(dbl); - const sc1 = newA.subtract(newA.subtract(p1).divide(1 - t)); - // Find new point on c2--e. - const p2 = newB.add(dbr); - const sc2 = newA.add(p2.subtract(newA).divide(t)); - // Construct new c1` based on the fact that s--sc1 is s--c1 * t. - const nc1 = points[0].add(sc1.subtract(points[0]).divide(t)); - // Construct new c2` based on the fact that e--sc2 is e--c2 * (1-t). - const nc2 = points[3].subtract(points[3].subtract(sc2).divide(1 - t)); - - const curve = editPath.curves[this.hitCurveInfo.curveIndex]; - curve.handle1 = nc1.subtract(points[0]); - curve.handle2 = nc2.subtract(points[3]); - PaperUtil.replacePathInStore(this.ps, this.editPathId, editPath.pathData); - } - - // @Override - onKeyDown(event: paper.KeyEvent) { - // TODO: react to escape key presses? - } -} - -// TODO: rename this... -function lli( - { x: x1, y: y1 }: paper.Point, - { x: x2, y: y2 }: paper.Point, - { x: x3, y: y3 }: paper.Point, - { x: x4, y: y4 }: paper.Point, -) { - const nx = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4); - const ny = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4); - const d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); - if (d === 0) { - return undefined; - } - return new paper.Point(nx / d, ny / d); -} - -function hull(points: CubicPoints, t: number): CubicHull { - let p = points as paper.Point[]; - let _p: paper.Point[] = []; - let pt: paper.Point; - const q = [...p]; - // We lerp between all points at each iteration, until we have 1 point left. - while (p.length > 1) { - _p = []; - for (let i = 0, l = p.length - 1; i < l; i++) { - pt = p[i].add(p[i + 1].subtract(p[i]).multiply(t)); - q.push(pt); - _p.push(pt); - } - p = _p; - } - return q as CubicHull; -} - -type CubicPoints = [paper.Point, paper.Point, paper.Point, paper.Point]; -type CubicHull = [ - paper.Point, - paper.Point, - paper.Point, - paper.Point, - paper.Point, - paper.Point, - paper.Point, - paper.Point, - paper.Point, - paper.Point -]; diff --git a/src/app/pages/editor/scripts/paper/gesture/edit/SelectDragDrawSegmentsGesture.ts b/src/app/pages/editor/scripts/paper/gesture/edit/SelectDragDrawSegmentsGesture.ts deleted file mode 100644 index a5cf9720..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/edit/SelectDragDrawSegmentsGesture.ts +++ /dev/null @@ -1,243 +0,0 @@ -import { CursorType } from 'app/pages/editor/model/paper'; -import { MathUtil } from 'app/pages/editor/scripts/common'; -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil, SnapUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import { Line } from 'app/pages/editor/store/paper/actions'; -import * as paper from 'paper'; - -/** - * A gesture that performs selection and drag operations on one or more path - * segments. It also supports adding a segment to an existing path's curve, - * as well as extending an open path by appending segments to its end points. - * - * Preconditions: - * - The user is in edit path mode. - * - The user either hit a segment, a curve, or missed entirely - * (the 'missed entirely' case occurs when the user is in vector - * mode, in which the user can create new segments by clicking - * on the canvas). - */ -export class SelectDragDrawSegmentsGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - - // Maps segment indices to each segment's location at the beginning of the gesture. - // The initial locations are expressed in the - private selectedSegmentIndexToInitialLocationMap: ReadonlyMap; - // The location of the last mouse event in the edit path's local coordinates. - private localLastPoint: paper.Point; - // True if we should exit edit path mode on the next mouse up event. - private exitEditPathModeOnMouseUp = false; - // If this gesture was used to split a curve, this tells us the index of the - // new segment that was created in onMouseDown(). - private newSplitCurveSegmentIndex: number; - - /** Static factory method to use when the user's mouse down hits a segment. */ - static hitSegment(ps: PaperService, editPathId: string, segmentIndex: number) { - return new SelectDragDrawSegmentsGesture(ps, editPathId, { segmentIndex }); - } - - /** Static factory method to use when the user's mouse down hits a curve. */ - static hitCurve(ps: PaperService, editPathId: string, curveIndex: number, time: number) { - return new SelectDragDrawSegmentsGesture(ps, editPathId, undefined, { curveIndex, time }); - } - - /** Static factory method to use when the user misses the edit path. */ - static miss(ps: PaperService, editPathId: string) { - return new SelectDragDrawSegmentsGesture(ps, editPathId); - } - - private constructor( - private readonly ps: PaperService, - private readonly editPathId: string, - private readonly hitSegmentInfo?: Readonly<{ segmentIndex: number }>, - private readonly hitCurveInfo?: Readonly<{ curveIndex?: number; time: number }>, - ) { - super(); - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - const fpi = this.ps.getEditPathInfo(); - const beforeSelectedSegmentIndices = fpi.selectedSegments; - const afterSelectedSegmentIndices = new Set(beforeSelectedSegmentIndices); - const editPath = this.pl.findItemByLayerId(this.editPathId) as paper.Path; - - if (this.hitSegmentInfo) { - const isEndPointFn = (i: number) => i === 0 || i === editPath.segments.length - 1; - const { segmentIndex } = this.hitSegmentInfo; - const singleSelectedSegmentIndex = beforeSelectedSegmentIndices.size - ? beforeSelectedSegmentIndices.values().next().value - : undefined; - if ( - !editPath.closed && - singleSelectedSegmentIndex !== undefined && - isEndPointFn(segmentIndex) && - isEndPointFn(singleSelectedSegmentIndex) && - segmentIndex !== singleSelectedSegmentIndex - ) { - // If the path is open, one of the end points is selected, and the - // user clicked the other end point segment, then close the path - // and end the gesture on the next mouse up event. - editPath.closed = true; - this.exitEditPathModeOnMouseUp = true; - PaperUtil.replacePathInStore(this.ps, this.editPathId, editPath.pathData); - } - if (event.modifiers.shift || event.modifiers.command) { - // If shift or command is pressed, toggle the segment's selection state. - if (beforeSelectedSegmentIndices.has(segmentIndex)) { - afterSelectedSegmentIndices.delete(segmentIndex); - } else { - afterSelectedSegmentIndices.add(segmentIndex); - } - } else { - // Otherwise, select the hit segment and deselect all others. - afterSelectedSegmentIndices.clear(); - afterSelectedSegmentIndices.add(segmentIndex); - } - } else if (this.hitCurveInfo) { - // If there is no hit segment, then create one along the curve - // at the given location and select the new segment. - const { curveIndex, time } = this.hitCurveInfo; - const curve = editPath.curves[curveIndex]; - const newSegment = event.modifiers.shift - ? curve.divideAt(curve.getLocationAt(curve.length / 2)) - : curve.divideAtTime(time).segment1; - PaperUtil.replacePathInStore(this.ps, this.editPathId, editPath.pathData); - afterSelectedSegmentIndices.clear(); - afterSelectedSegmentIndices.add(newSegment.index); - this.newSplitCurveSegmentIndex = newSegment.index; - } else { - // Otherwise, we are either (1) extending an existing open path (beginning - // at one of its selected end points), or (2) beginning to create a new path - // from scratch. - const localPoint = editPath.globalToLocal(event.point); - let addedSegment: paper.Segment; - if (editPath.segments.length === 0) { - addedSegment = editPath.add(localPoint); - } else { - // Note that there will always be a single selected end point segment in this case - // (otherwise we would have used a batch select segments gesture instead). - const singleSelectedSegmentIndex = beforeSelectedSegmentIndices.values().next().value; - const selectedSegment = editPath.segments[singleSelectedSegmentIndex]; - addedSegment = selectedSegment.isLast() - ? editPath.add(localPoint) - : editPath.insert(0, localPoint); - afterSelectedSegmentIndices.delete(singleSelectedSegmentIndex); - } - afterSelectedSegmentIndices.add(addedSegment.index); - PaperUtil.replacePathInStore(this.ps, this.editPathId, editPath.pathData); - } - - this.selectedSegmentIndexToInitialLocationMap = new Map( - Array.from(afterSelectedSegmentIndices).map(segmentIndex => { - const point = editPath.segments[segmentIndex].point.clone(); - return [segmentIndex, point] as [number, paper.Point]; - }), - ); - - this.ps.setEditPathInfo({ - ...fpi, - ...PaperUtil.selectCurves(editPath, afterSelectedSegmentIndices), - }); - this.ps.setCursorType(CursorType.PointSelect); - this.ps.setCreatePathInfo(undefined); - this.ps.setSplitCurveInfo(undefined); - } - - // @Override - onMouseDrag(event: paper.ToolEvent) { - const editPath = this.pl.findItemByLayerId(this.editPathId) as paper.Path; - - const localDownPoint = editPath.globalToLocal(event.downPoint); - if (!this.localLastPoint) { - this.localLastPoint = localDownPoint; - } - const localPoint = editPath.globalToLocal(event.point); - const localDownPointDelta = localPoint.subtract(localDownPoint); - const localLastPointDelta = localPoint.subtract(this.localLastPoint); - const localSnappedDownPointDelta = new paper.Point( - MathUtil.snapVectorToAngle(localDownPointDelta, 90), - ); - - if (this.hitSegmentInfo || this.hitCurveInfo) { - // A segment was created on mouse down and is still being grabbed, - // so continue to drag the currently selected segments. - const selectedSegmentIndices = new Set(this.selectedSegmentIndexToInitialLocationMap.keys()); - const nonSelectedSegmentIndices = editPath.segments - .map((s, i) => i) - .filter((s, i) => !selectedSegmentIndices.has(i)); - this.selectedSegmentIndexToInitialLocationMap.forEach((initialSegmentPoint, i) => { - const segment = editPath.segments[i]; - segment.point = event.modifiers.shift - ? initialSegmentPoint.add(localSnappedDownPointDelta) - : segment.point.add(localLastPointDelta); - }); - const draggedSegmentIndex = this.hitSegmentInfo - ? this.hitSegmentInfo.segmentIndex - : this.newSplitCurveSegmentIndex; - const dragSnapPoint = editPath.localToGlobal(editPath.segments[draggedSegmentIndex].point); - const { topLeft, center, bottomRight } = Array.from( - this.selectedSegmentIndexToInitialLocationMap.values(), - ).reduce((rect: paper.Rectangle, p: paper.Point) => { - p = editPath.localToGlobal(p); - return rect ? rect.include(p) : new paper.Rectangle(p, new paper.Size(0, 0)); - }, undefined); - const siblingSnapPointsTable = [ - [topLeft, center, bottomRight], - ...nonSelectedSegmentIndices.map(i => { - return [editPath.localToGlobal(editPath.segments[i].point)]; - }), - ]; - - // TODO: snap this stuff like we do in the other gestures! - const snapInfo = SnapUtil.computeSnapInfo([dragSnapPoint], siblingSnapPointsTable); - if (snapInfo) { - this.ps.setSnapGuideInfo({ - guides: snapInfo.guides.map(({ from, to }: Line) => { - from = this.pl.globalToLocal(new paper.Point(from)); - to = this.pl.globalToLocal(new paper.Point(to)); - return { from, to }; - }), - rulers: [], - }); - } - } else { - // Then we have just added a segment to the path in onMouseDown() - // and should thus move the segment's handles onMouseDrag(). - // Note that there will only ever be one selected segment in this case. - const selectedSegmentIndex = this.selectedSegmentIndexToInitialLocationMap.keys().next() - .value; - const selectedSegment = editPath.segments[selectedSegmentIndex]; - // TODO: dragging a handle belonging to an endpoint doesn't work! handle info is lost! - // TODO: snap the dragged segment handle with the newly created segment - if (event.modifiers.shift) { - const index = selectedSegmentIndex; - const initialSelectedSegmentPosition = this.selectedSegmentIndexToInitialLocationMap.get( - index, - ); - const delta = localSnappedDownPointDelta; - selectedSegment.handleIn = initialSelectedSegmentPosition.subtract(delta); - selectedSegment.handleOut = initialSelectedSegmentPosition.add(delta); - } else { - selectedSegment.handleIn = selectedSegment.handleIn.subtract(localLastPointDelta); - selectedSegment.handleOut = selectedSegment.handleOut.add(localLastPointDelta); - } - } - this.localLastPoint = localPoint; - - PaperUtil.replacePathInStore(this.ps, this.editPathId, editPath.pathData); - } - - // @Override - onMouseUp(event: paper.ToolEvent) { - this.ps.setCursorType(CursorType.Default); - this.ps.setSnapGuideInfo(undefined); - if (this.exitEditPathModeOnMouseUp) { - this.ps.setEditPathInfo(undefined); - } - } - - // TODO: handle escape key event? -} diff --git a/src/app/pages/editor/scripts/paper/gesture/edit/SelectDragHandleGesture.ts b/src/app/pages/editor/scripts/paper/gesture/edit/SelectDragHandleGesture.ts deleted file mode 100644 index 3c1a60f9..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/edit/SelectDragHandleGesture.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -/** - * A gesture that performs selection and drag operations on a segment handle. - * - * Preconditions: - * - The user is in edit path mode. - * - The user hit a segment handle. - */ -export class SelectDragHandleGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - private readonly hitHandleType: 'handleIn' | 'handleOut'; - - // The handle's initial location in local coordinates. - private localDownHandle: paper.Point; - private localLastDragInfo: { - readonly localDownPoint: paper.Point; - readonly localPoint: paper.Point; - }; - - constructor( - private readonly ps: PaperService, - private readonly editPathId: string, - private readonly hitSegmentIndex: number, - hitResultType: 'handle-in' | 'handle-out', - ) { - super(); - this.hitHandleType = hitResultType === 'handle-in' ? 'handleIn' : 'handleOut'; - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - // Deselect all currently selected segments. - const selectedSegments = new Set(); - const selectedHandleIn = this.hitHandleType === 'handleIn' ? this.hitSegmentIndex : undefined; - const selectedHandleOut = this.hitHandleType === 'handleOut' ? this.hitSegmentIndex : undefined; - this.ps.setEditPathInfo({ - ...this.ps.getEditPathInfo(), - selectedSegments, - selectedHandleIn, - selectedHandleOut, - }); - const editPath = this.pl.findItemByLayerId(this.editPathId) as paper.Path; - this.localDownHandle = editPath.segments[this.hitSegmentIndex][this.hitHandleType].clone(); - } - - // @Override - onMouseDrag(event: paper.ToolEvent) { - const editPath = this.pl.findItemByLayerId(this.editPathId); - this.localLastDragInfo = { - localDownPoint: editPath.globalToLocal(event.downPoint), - localPoint: editPath.globalToLocal(event.point), - }; - this.processEvent(event); - } - - // @Override - onKeyDown(event: paper.KeyEvent) { - this.processKeyEvent(event); - } - - // @Override - onKeyUp(event: paper.KeyEvent) { - this.processKeyEvent(event); - } - - // TODO: react to 'escape' key presses - private processKeyEvent(event: paper.KeyEvent) { - if (event.key === 'shift') { - this.processEvent(event); - } - } - - private processEvent(event: paper.Event) { - if (!this.localLastDragInfo) { - return; - } - // TODO: add 'straight', 'mirrored', 'disconnected', and 'asymmetric' modes (similar to Sketch) - const { localDownPoint, localPoint } = this.localLastDragInfo; - const localHandle = this.localDownHandle.add(localPoint.subtract(localDownPoint)); - if (event.modifiers.shift) { - // Project the handle onto the handle's original vector. - const theta = (-(this.localDownHandle.angle - localHandle.angle) * Math.PI) / 180; - localHandle.set( - this.localDownHandle.normalize().multiply(localHandle.length * Math.cos(theta)), - ); - } - const editPath = this.pl.findItemByLayerId(this.editPathId) as paper.Path; - editPath.segments[this.hitSegmentIndex][this.hitHandleType] = localHandle; - PaperUtil.replacePathInStore(this.ps, this.editPathId, editPath.pathData); - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/edit/ToggleSegmentHandlesGesture.ts b/src/app/pages/editor/scripts/paper/gesture/edit/ToggleSegmentHandlesGesture.ts deleted file mode 100644 index b84694dd..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/edit/ToggleSegmentHandlesGesture.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -/** - * A gesture that toggles the handles associated with a path segment. - * - * Preconditions: - * - The user is in edit path mode. - * - The gesture began with a mouse down event on top of a segment - * (typically this is the second mouse down of a double click). - */ -export class ToggleSegmentHandlesGesture extends Gesture { - constructor( - private readonly ps: PaperService, - private readonly editPathId: string, - private readonly hitSegmentIndex: number, - ) { - super(); - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - const path = new paper.Path(PaperUtil.getPathFromStore(this.ps, this.editPathId)); - const segment = path.segments[this.hitSegmentIndex]; - if (segment.hasHandles()) { - segment.clearHandles(); - } else { - // TODO: polish this a bit more using the extra options argument? - segment.smooth(); - } - PaperUtil.replacePathInStore(this.ps, this.editPathId, path.pathData); - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/edit/index.ts b/src/app/pages/editor/scripts/paper/gesture/edit/index.ts deleted file mode 100644 index 9ea9c7a0..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/edit/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { BatchSelectSegmentsGesture } from './BatchSelectSegmentsGesture'; -export { MouldCurveGesture } from './MouldCurveGesture'; -export { SelectDragDrawSegmentsGesture } from './SelectDragDrawSegmentsGesture'; -export { SelectDragHandleGesture } from './SelectDragHandleGesture'; -export { ToggleSegmentHandlesGesture } from './ToggleSegmentHandlesGesture'; diff --git a/src/app/pages/editor/scripts/paper/gesture/hover/HoverGesture.ts b/src/app/pages/editor/scripts/paper/gesture/hover/HoverGesture.ts deleted file mode 100644 index f928a849..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/hover/HoverGesture.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ToolMode } from 'app/pages/editor/model/paper'; -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -import { HoverItemsGesture } from './HoverItemsGesture'; -import { HoverSegmentsCurvesGesture } from './HoverSegmentsCurvesGesture'; - -/** - * A gesture that handles mouse move hover events. - */ -export class HoverGesture extends Gesture { - private readonly hoverItemsGesture = new HoverItemsGesture(this.ps); - private readonly hoverSegmentsCurvesGesture = new HoverSegmentsCurvesGesture(this.ps); - - constructor(private readonly ps: PaperService) { - super(); - } - - // @Override - onMouseMove(event: paper.ToolEvent) { - const gesture = this.getCurrentGesture(); - if (gesture) { - gesture.onMouseMove(event); - } - } - - // @Override - onKeyDown(event: paper.KeyEvent) { - const gesture = this.getCurrentGesture(); - if (gesture) { - gesture.onKeyDown(event); - } - } - - private getCurrentGesture() { - if (this.ps.getToolMode() !== ToolMode.Default) { - return undefined; - } - const epi = this.ps.getEditPathInfo(); - if (!epi) { - return this.hoverItemsGesture; - } - if (!this.ps.getSelectedLayerIds().size) { - // If we are in edit path mode but there is no selected layer ID, then - // the user is using the 'vector' tool and hasn't yet started to create - // a path. In this case we do not want to show any hovers. - return undefined; - } - return this.hoverSegmentsCurvesGesture; - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/hover/HoverItemsGesture.ts b/src/app/pages/editor/scripts/paper/gesture/hover/HoverItemsGesture.ts deleted file mode 100644 index 45024221..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/hover/HoverItemsGesture.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { CursorType } from 'app/pages/editor/model/paper'; -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { HitTests } from 'app/pages/editor/scripts/paper/item'; -import { PivotType } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -// prettier-ignore -const RESIZE_CURSOR_MAP: ReadonlyMap = new Map([ - ['bottomLeft', CursorType.Resize45], ['leftCenter', CursorType.Resize90], - ['topLeft', CursorType.Resize135], ['topCenter', CursorType.Resize0], - ['topRight', CursorType.Resize45], ['rightCenter', CursorType.Resize90], - ['bottomRight', CursorType.Resize135], ['bottomCenter', CursorType.Resize0], -] as [PivotType, CursorType][]); - -// prettier-ignore -const ROTATE_CURSOR_MAP: ReadonlyMap = new Map([ - ['bottomLeft', CursorType.Rotate225], ['leftCenter', CursorType.Rotate270], - ['topLeft', CursorType.Rotate315], ['topCenter', CursorType.Rotate0], - ['topRight', CursorType.Rotate45], ['rightCenter', CursorType.Rotate90], - ['bottomRight', CursorType.Rotate135], ['bottomCenter', CursorType.Rotate180], -] as [PivotType, CursorType][]); - -// prettier-ignore -const TRANSFORM_CURSOR_MAP: ReadonlyMap = new Map([ - ['bottomLeft', CursorType.Resize45], ['leftCenter', CursorType.Resize0], - ['topLeft', CursorType.Resize135], ['topCenter', CursorType.Resize90], - ['topRight', CursorType.Resize45], ['rightCenter', CursorType.Resize0], - ['bottomRight', CursorType.Resize135], ['bottomCenter', CursorType.Resize90], -] as [PivotType, CursorType][]); - -/** - * A gesture that performs hover operations on items. - * - * Preconditions: - * - The user is in default, rotate items, or transform paths mode. - */ -export class HoverItemsGesture extends Gesture { - constructor(private readonly ps: PaperService) { - super(); - } - - // @Override - onMouseMove(event: paper.ToolEvent) { - this.ps.setCursorType(CursorType.Default); - - const selectedLayers = this.ps.getSelectedLayerIds(); - if (selectedLayers.size) { - const rii = this.ps.getRotateItemsInfo(); - const selectionBoundSegmentsHitResult = HitTests.selectionModeSegments(event.point); - if (selectionBoundSegmentsHitResult) { - const tpi = this.ps.getTransformPathsInfo(); - const cursorMap = rii ? ROTATE_CURSOR_MAP : tpi ? TRANSFORM_CURSOR_MAP : RESIZE_CURSOR_MAP; - this.ps.setCursorType(cursorMap.get(selectionBoundSegmentsHitResult.item.pivotType)); - this.ps.setHoveredLayerId(undefined); - return; - } - if (rii && HitTests.rotateItemsPivot(event.point)) { - this.ps.setCursorType(CursorType.Grab); - this.ps.setHoveredLayerId(undefined); - return; - } - } - - const hitResult = HitTests.selectionMode(event.point, this.ps); - if (hitResult && !selectedLayers.has(hitResult.hitItem.data.id)) { - this.ps.setHoveredLayerId(hitResult.hitItem.data.id); - } else { - this.ps.setHoveredLayerId(undefined); - } - } - - // @Override - onKeyDown(event: paper.KeyEvent) { - switch (event.key) { - case 'escape': - // TODO: also do this in any other hover/pen/pencil related gestures? - this.ps.setCursorType(CursorType.Default); - this.ps.setSnapGuideInfo(undefined); - this.ps.setRotateItemsInfo(undefined); - this.ps.setTransformPathsInfo(undefined); - break; - case 'backspace': - case 'delete': - this.ps.deleteSelectedModels(); - break; - } - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/hover/HoverSegmentsCurvesGesture.ts b/src/app/pages/editor/scripts/paper/gesture/hover/HoverSegmentsCurvesGesture.ts deleted file mode 100644 index 41ecaf0e..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/hover/HoverSegmentsCurvesGesture.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { CursorType } from 'app/pages/editor/model/paper'; -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { HitTests, PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import { Action } from 'app/pages/editor/store'; -import { BatchAction } from 'app/pages/editor/store/batch/actions'; -import { SetVectorLayer } from 'app/pages/editor/store/layers/actions'; -import { SetEditPathInfo } from 'app/pages/editor/store/paper/actions'; -import * as paper from 'paper'; - -/** - * A gesture that performs hover operations over segments and curves. - * - * Preconditions: - * - The user is in edit path mode for a selected layer id. - */ -export class HoverSegmentsCurvesGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - - constructor(private readonly ps: PaperService) { - super(); - } - - // @Override - onMouseMove(event: paper.ToolEvent) { - this.ps.setCursorType(CursorType.Default); - this.ps.setSplitCurveInfo(undefined); - - // TODO: this seems kinda hacky - // TODO: currently necessary (if the previous gesture was the create/drag/draw segments gesture) - this.ps.setCreatePathInfo(undefined); - - const editPathId = this.ps - .getSelectedLayerIds() - .values() - .next().value; - const editPath = this.pl.findItemByLayerId(editPathId) as paper.Path; - const segmentsAndHandlesHitResult = HitTests.editPathModeSegmentsAndHandles(event.point); - if (segmentsAndHandlesHitResult) { - // If we are hovering over a segment or a handle, then show a point select - // cursor and return. - this.ps.setCursorType(CursorType.PointSelect); - return; - } - - const editPathHitResult = HitTests.editPathMode(event.point, editPath, { - curves: true, - }); - if (editPathHitResult) { - if (editPathHitResult.type !== 'curve') { - // If we hit the edit path but missed its segments/handles/curves, - // then do nothing. - return; - } - // Show a pen add cursor and highlight the curve the user is about to split. - this.ps.setCursorType(CursorType.PenAdd); - const hitCurve = editPathHitResult.location.curve; - const location = event.modifiers.shift - ? hitCurve.getLocationAt(hitCurve.length / 2) - : editPathHitResult.location; - const vpSplitPoint = this.localToVpPoint(editPath, location.point); - const { point: p1, handleIn: in1, handleOut: out1 } = this.localToVpSegment( - editPath, - location.curve.segment1, - ); - const { point: p2, handleIn: in2, handleOut: out2 } = this.localToVpSegment( - editPath, - location.curve.segment2, - ); - this.ps.setSplitCurveInfo({ - splitPoint: vpSplitPoint, - segment1: { point: p1, handleIn: in1, handleOut: out1 }, - segment2: { point: p2, handleIn: in2, handleOut: out2 }, - }); - return; - } - - // Draw an 'extend path' preview curve if one of its end points - // is selected and the path is still open. - const singleSelectedSegmentIndex = this.findSingleSelectedEndSegmentIndex(editPath); - if (singleSelectedSegmentIndex !== undefined) { - this.ps.setCursorType(CursorType.PenAdd); - const vpStartSegment = this.localToVpSegment( - editPath, - editPath.segments[singleSelectedSegmentIndex], - ); - const vpEndSegment = new paper.Segment(this.pl.globalToLocal(event.point)); - const { pathData } = new paper.Path([vpStartSegment, vpEndSegment]); - this.ps.setCreatePathInfo({ - pathData, - strokeColor: '#979797', - }); - } - } - - /** Converts local coordinates to viewport coordinates for a point. */ - private localToVpPoint(localItem: paper.Item, localPoint: paper.Point) { - return localPoint ? this.pl.globalToLocal(localItem.localToGlobal(localPoint)) : undefined; - } - - /** Converts local coordinates to viewport coordinates for a segment. */ - private localToVpSegment(localItem: paper.Item, localSegment: paper.Segment) { - return new paper.Segment( - this.localToVpPoint(localItem, localSegment.point), - this.localToVpHandle(localItem, localSegment.point, localSegment.handleIn), - this.localToVpHandle(localItem, localSegment.point, localSegment.handleOut), - ); - } - - /** Converts local coordinates to viewport coordinates for a segment handle. */ - private localToVpHandle( - localItem: paper.Item, - localPoint: paper.Point, - localHandle: paper.Point, - ) { - const vpPoint = this.localToVpPoint(localItem, localPoint); - const vpHandle = this.localToVpPoint(localItem, localPoint.add(localHandle)); - return vpHandle.subtract(vpPoint); - } - - /** - * Returns the single selected end point segment index for the given path, - * or undefined if one doesn't exist. - */ - private findSingleSelectedEndSegmentIndex(path: paper.Path) { - if (path.closed) { - // Return undefined if the path is closed. - return undefined; - } - const { selectedSegments } = this.ps.getEditPathInfo(); - if (selectedSegments.size !== 1) { - // Return undefined if there is not a single selected segment. - return undefined; - } - const lastIndex = path.segments.length - 1; - return selectedSegments.has(0) ? 0 : selectedSegments.has(lastIndex) ? lastIndex : undefined; - } - - // @Override - onKeyDown(event: paper.KeyEvent) { - switch (event.key) { - case 'escape': - // TODO: also do this in any other hover/pen/pencil related gestures? - this.ps.exitEditPathMode(); - break; - case 'backspace': - case 'delete': - this.deleteSelectedSegmentsAndHandles(); - break; - } - } - - private deleteSelectedSegmentsAndHandles() { - const layerId = this.ps - .getSelectedLayerIds() - .values() - .next().value; - const { selectedHandleIn, selectedHandleOut, selectedSegments } = this.ps.getEditPathInfo(); - if ( - selectedHandleIn === undefined && - selectedHandleOut === undefined && - selectedSegments.size === 0 - ) { - // Do nothing if there are no selected segments/handles. - // TODO: should we delete the layer in this case? - return; - } - const editPath = this.pl.findItemByLayerId(layerId) as paper.Path; - for (let i = editPath.segments.length - 1; i >= 0; i--) { - const segment = editPath.segments[i]; - if (selectedSegments.has(i)) { - segment.remove(); - continue; - } - if (selectedHandleIn === i) { - segment.handleIn = undefined; - } - if (selectedHandleOut === i) { - segment.handleOut = undefined; - } - } - const actions: Action[] = []; - if (editPath.segments.length === 0) { - // Delete the layer and exit edit path mode if there are no segments remaining. - actions.push(...this.ps.getDeleteSelectedModelsActions()); - actions.push(...this.ps.getExitEditPathModeActions()); - } else { - actions.push( - new SetVectorLayer( - PaperUtil.getReplacePathInStoreVectorLayer(this.ps, layerId, editPath.pathData), - ), - new SetEditPathInfo({ - selectedHandleIn: undefined, - selectedHandleOut: undefined, - selectedSegments: new Set(), - visibleHandleIns: new Set(), - visibleHandleOuts: new Set(), - }), - ...this.ps.getClearEditPathModeStateActions(), - ); - } - this.ps.dispatchStore(new BatchAction(...actions)); - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/hover/index.ts b/src/app/pages/editor/scripts/paper/gesture/hover/index.ts deleted file mode 100644 index 574399b4..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/hover/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { HoverGesture } from './HoverGesture'; diff --git a/src/app/pages/editor/scripts/paper/gesture/index.ts b/src/app/pages/editor/scripts/paper/gesture/index.ts deleted file mode 100644 index 297dc84c..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Gesture } from './Gesture'; diff --git a/src/app/pages/editor/scripts/paper/gesture/rotate/RotateItemsDragPivotGesture.ts b/src/app/pages/editor/scripts/paper/gesture/rotate/RotateItemsDragPivotGesture.ts deleted file mode 100644 index 047fd1e0..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/rotate/RotateItemsDragPivotGesture.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { CursorType } from 'app/pages/editor/model/paper'; -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -/** - * A gesture that performs rotation operations. - * - * Preconditions: - * - The user is in default mode. - * - One or more layers are selected. - * - A mouse down event occurred on top of the rotate items pivot. - */ -export class RotateItemsDragPivotGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - private vpInitialPivotPoint: paper.Point; - private vpDownPoint: paper.Point; - - constructor(private readonly ps: PaperService) { - super(); - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - this.ps.setCursorType(CursorType.Grabbing); - - // TODO: reuse this code with PaperLayer (filter out empty groups) - const selectedItems = Array.from(this.ps.getSelectedLayerIds()) - .map(id => this.pl.findItemByLayerId(id)) - .filter(i => !(i instanceof paper.Group) || i.children.length); - const invertedPaperLayerMatrix = this.pl.matrix.inverted(); - const rii = this.ps.getRotateItemsInfo(); - if (rii.pivot) { - this.vpInitialPivotPoint = new paper.Point(rii.pivot); - } else { - this.vpInitialPivotPoint = PaperUtil.transformRectangle( - PaperUtil.computeBounds(selectedItems), - invertedPaperLayerMatrix, - ).center; - } - this.vpDownPoint = this.pl.globalToLocal(event.downPoint); - } - - // @Override - onMouseDrag(event: paper.ToolEvent) { - const vpPoint = this.pl.globalToLocal(event.point); - const pivot = this.vpInitialPivotPoint.add(vpPoint.subtract(this.vpDownPoint)); - this.ps.setRotateItemsInfo({ pivot }); - } - - // @Override - onMouseUp(event: paper.ToolEvent) { - this.ps.setCursorType(CursorType.Default); - } - - // @Override - onKeyDown(event: paper.KeyEvent) {} - - // @Override - onKeyUp(event: paper.KeyEvent) {} -} diff --git a/src/app/pages/editor/scripts/paper/gesture/rotate/RotateItemsGesture.ts b/src/app/pages/editor/scripts/paper/gesture/rotate/RotateItemsGesture.ts deleted file mode 100644 index 2a9837f6..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/rotate/RotateItemsGesture.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { LayerUtil, PathLayer, VectorLayer } from 'app/pages/editor/model/layers'; -import { Path } from 'app/pages/editor/model/paths'; -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -/** - * A gesture that performs rotation operations. - * - * Preconditions: - * - The user is in default mode. - * - One or more layers are selected. - * - A mouse down event occurred on a selection bounds handle. - * - * TODO: avoid jank at beginning of rotation (when angle is near 0) - * TODO: make sure the 'empty group' logic we add also matches what we have in PaperLayer.ts - * TODO: show a tool tip during rotations - * TODO: make sure the pivot doesn't move during the initial drag - */ -export class RotateItemsGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - private selectedItems: ReadonlyArray; - private localToVpItemMatrices: ReadonlyArray; - private initialVectorLayer: VectorLayer; - private vpPivot: paper.Point; - private vpDownPoint: paper.Point; - private vpPoint: paper.Point; - - constructor(private readonly ps: PaperService) { - super(); - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - this.ps.setHoveredLayerId(undefined); - - const scaleItems: paper.Item[] = []; - const scaleItemsSet = new Set(); - Array.from(this.ps.getSelectedLayerIds()) - .map(id => this.pl.findItemByLayerId(id)) - // TODO: reuse this code with PaperLayer (filter out empty groups) - .filter(i => !(i instanceof paper.Group) || i.children.length) - .forEach(function recurseFn(i: paper.Item) { - if (i instanceof paper.Group) { - i.children.forEach(recurseFn); - } else if (!scaleItemsSet.has(i.data.id)) { - scaleItemsSet.add(i.data.id); - scaleItems.push(i); - } - }); - this.selectedItems = scaleItems; - - const invertedPaperLayerMatrix = this.pl.matrix.inverted(); - this.localToVpItemMatrices = this.selectedItems.map(item => { - // Compute the matrices to directly transform during drag events. - return item.globalMatrix.prepended(invertedPaperLayerMatrix).inverted(); - }); - const rii = this.ps.getRotateItemsInfo(); - if (rii.pivot) { - this.vpPivot = new paper.Point(rii.pivot); - } else { - this.vpPivot = PaperUtil.transformRectangle( - PaperUtil.computeBounds(this.selectedItems), - invertedPaperLayerMatrix, - ).center; - } - this.initialVectorLayer = this.ps.getVectorLayer(); - this.vpDownPoint = this.pl.globalToLocal(event.downPoint); - } - - // @Override - onMouseDrag(event: paper.ToolEvent) { - this.vpPoint = this.pl.globalToLocal(event.point); - this.processEvent(event); - } - - // @Override - onKeyDown(event: paper.KeyEvent) { - this.processKeyEvent(event); - } - - // @Override - onKeyUp(event: paper.KeyEvent) { - this.processKeyEvent(event); - } - - private processKeyEvent(event: paper.KeyEvent) { - if (event.key === 'shift') { - this.processEvent(event); - } - } - - // TODO: determine if we should be baking transforms into the children layers when rotating a group? - private processEvent(event: paper.Event) { - if (!this.vpPoint) { - return; - } - const rotationAngle = this.getRotationAngle(event); - let newVl = this.initialVectorLayer.clone(); - this.selectedItems.forEach((item, index) => { - const path = item.clone() as paper.Path; - path.applyMatrix = true; - const localToViewportMatrix = this.localToVpItemMatrices[index]; - const matrix = localToViewportMatrix.clone(); - matrix.rotate(rotationAngle, this.vpPivot); - matrix.append(localToViewportMatrix.inverted()); - path.matrix = matrix; - const newPl = newVl.findLayerById(item.data.id).clone() as PathLayer; - newPl.pathData = new Path(path.pathData); - newVl = LayerUtil.replaceLayer(newVl, item.data.id, newPl); - }); - this.ps.setVectorLayer(newVl); - } - - private getRotationAngle(event: paper.Event) { - const initialDelta = this.vpDownPoint.subtract(this.vpPivot); - const initialAngle = (Math.atan2(initialDelta.y, initialDelta.x) * 180) / Math.PI; - const delta = this.vpPoint.subtract(this.vpPivot); - const angle = (Math.atan2(delta.y, delta.x) * 180) / Math.PI - initialAngle; - // TODO: this doesn't round properly if the angle was previously changed - return event.modifiers.shift ? Math.round(angle / 15) * 15 : angle; - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/rotate/index.ts b/src/app/pages/editor/scripts/paper/gesture/rotate/index.ts deleted file mode 100644 index 7db0eac0..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/rotate/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { RotateItemsGesture } from './RotateItemsGesture'; -export { RotateItemsDragPivotGesture } from './RotateItemsDragPivotGesture'; diff --git a/src/app/pages/editor/scripts/paper/gesture/scale/ScaleItemsGesture.ts b/src/app/pages/editor/scripts/paper/gesture/scale/ScaleItemsGesture.ts deleted file mode 100644 index 3a39ee2c..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/scale/ScaleItemsGesture.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { LayerUtil, MorphableLayer, VectorLayer } from 'app/pages/editor/model/layers'; -import { Path } from 'app/pages/editor/model/paths'; -import { MathUtil } from 'app/pages/editor/scripts/common'; -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer, SelectionBoundsRaster } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil, SnapUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import { Line } from 'app/pages/editor/store/paper/actions'; -import * as _ from 'lodash'; -import * as paper from 'paper'; - -/** - * A gesture that performs scaling operations. - * - * Preconditions: - * - The user is in default mode. - * - One or more layers are selected. - * - A mouse down event occurred on a selection bounds handle. - * - * TODO: should we also scale the stroke width? - */ -export class ScaleItemsGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - private selectedItems: ReadonlyArray; - private localToVpItemMatrices: ReadonlyArray; - private vpInitialPivot: paper.Point; - private vpInitialSize: paper.Point; - private vpInitialCenteredSize: paper.Point; - private vpInitialCenter: paper.Point; - private vpInitialDraggedSegment: paper.Point; - private vpDownPoint: paper.Point; - private vpPoint: paper.Point; - private initialVectorLayer: VectorLayer; - - constructor( - private readonly ps: PaperService, - private readonly selectionBoundsRaster: SelectionBoundsRaster, - ) { - super(); - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - this.ps.setHoveredLayerId(undefined); - - // TODO: make searches like this more efficient... - const scaleItems: paper.Item[] = []; - const scaleItemsSet = new Set(); - Array.from(this.ps.getSelectedLayerIds()) - .map(id => this.pl.findItemByLayerId(id)) - // TODO: reuse this code with PaperLayer (filter out empty groups) - .filter(i => !(i instanceof paper.Group) || i.children.length) - .forEach(function recurseFn(i: paper.Item) { - if (i instanceof paper.Group) { - i.children.forEach(recurseFn); - } else if (!scaleItemsSet.has(i.data.id)) { - scaleItemsSet.add(i.data.id); - scaleItems.push(i); - } - }); - this.selectedItems = scaleItems; - - this.localToVpItemMatrices = this.selectedItems.map(item => { - // Compute the matrices to directly transform during drag events. - return item.globalMatrix.prepended(this.pl.matrix.inverted()).inverted(); - }); - const bounds = PaperUtil.transformRectangle( - PaperUtil.computeBounds(this.selectedItems), - this.pl.matrix.inverted(), - ); - this.vpInitialPivot = bounds[this.selectionBoundsRaster.oppositePivotType]; - this.vpInitialDraggedSegment = bounds[this.selectionBoundsRaster.pivotType]; - this.vpDownPoint = bounds[this.selectionBoundsRaster.pivotType]; - this.vpPoint = this.vpDownPoint; - this.vpInitialSize = this.vpDownPoint.subtract(this.vpInitialPivot); - this.vpInitialCenteredSize = this.vpInitialSize.multiply(0.5); - this.vpInitialCenter = bounds.center.clone(); - this.initialVectorLayer = this.ps.getVectorLayer(); - } - - // @Override - onMouseDrag(event: paper.ToolEvent) { - this.vpPoint = this.pl.globalToLocal(event.point); - const { x, y } = this.vpPoint; - this.ps.setTooltipInfo({ - point: { x, y }, - // TODO: display the current width/height of the shape - label: `${_.round(x, 1)} ⨯ ${_.round(y, 1)}`, - }); - this.processEvent(event); - } - - // @Override - onMouseUp(event: paper.ToolEvent) { - // TODO: need to disable this in onKeyEvents as well? - this.ps.setSnapGuideInfo(undefined); - this.ps.setTooltipInfo(undefined); - } - - // @Override - onKeyDown(event: paper.KeyEvent) { - this.processKeyEvent(event); - } - - // @Override - onKeyUp(event: paper.KeyEvent) { - this.processKeyEvent(event); - } - - private processKeyEvent(event: paper.KeyEvent) { - if (event.key === 'alt' || event.key === 'shift') { - this.processEvent(event); - } - } - - // TODO: make sure it is possible to scale/shrink the item when holding shift? - private processEvent(event: paper.Event) { - const projDownPoint = this.pl.localToGlobal(this.vpDownPoint); - const projPoint = this.pl.localToGlobal(this.vpPoint); - const projDelta = projPoint.subtract(projDownPoint); - - let newVl = this.initialVectorLayer.clone(); - newVl = this.scaleItems(newVl, projDelta, event.modifiers.alt, event.modifiers.shift); - this.ps.setVectorLayer(newVl); - - // TODO: this could be WAY more efficient (no need to scale/snap things twice) - // TODO: snap if shift is held and aspect ratio doesn't change? - // TODO: first snap the widths and heights, then snap the guides - const shouldSnap = !event.modifiers.shift; - if (shouldSnap) { - const snapInfo = this.buildSnapInfo(); - if (snapInfo) { - const projSnapDelta = new paper.Point(snapInfo.projSnapDelta); - if (!projSnapDelta.isZero()) { - const shouldScaleAboutCenter = event.modifiers.alt; - const vpFixedPivot = shouldScaleAboutCenter ? this.vpInitialCenter : this.vpInitialPivot; - // TODO: confirm this is the correct way to fix the project snap delta? - if (this.vpPoint.x < vpFixedPivot.x) { - projSnapDelta.x *= -1; - } - if (this.vpPoint.y < vpFixedPivot.y) { - projSnapDelta.y *= -1; - } - newVl = this.scaleItems( - newVl, - projPoint.add(projSnapDelta).subtract(projDownPoint), - shouldScaleAboutCenter, - ); - this.ps.setVectorLayer(newVl); - } - } - } - - if (shouldSnap) { - const snapInfo = this.buildSnapInfo(); - if (snapInfo) { - this.ps.setSnapGuideInfo({ - guides: snapInfo.guides.map(g => this.projToVpLine(g)), - rulers: snapInfo.rulers.map(r => this.projToVpLine(r)), - }); - } else { - this.ps.setSnapGuideInfo(undefined); - } - } else { - this.ps.setSnapGuideInfo(undefined); - } - } - - private scaleItems( - newVl: VectorLayer, - projDelta: paper.Point, - shouldScaleAboutCenter: boolean, - shouldSnapDelta = false, - ) { - // Transform about the center if alt is pressed. Otherwise trasform about - // the pivot opposite of the currently active pivot. - const vpFixedPivot = shouldScaleAboutCenter ? this.vpInitialCenter : this.vpInitialPivot; - const currentSize = this.vpInitialDraggedSegment - .add(this.pl.globalToLocal(projDelta)) - .subtract(vpFixedPivot); - const initialSize = shouldScaleAboutCenter ? this.vpInitialCenteredSize : this.vpInitialSize; - let sx = 1; - let sy = 1; - if (!MathUtil.isNearZero(initialSize.x)) { - sx = currentSize.x / initialSize.x; - } - if (!MathUtil.isNearZero(initialSize.y)) { - sy = currentSize.y / initialSize.y; - } - if (shouldSnapDelta) { - const signx = sx > 0 ? 1 : -1; - const signy = sy > 0 ? 1 : -1; - sx = sy = Math.max(Math.abs(sx), Math.abs(sy)); - sx *= signx; - sy *= signy; - } - - // TODO: determine if we should be baking transforms into the children layers when scaling a group? - this.selectedItems.forEach((item, index) => { - const path = item.clone() as paper.Path; - path.applyMatrix = true; - const localToVpMatrix = this.localToVpItemMatrices[index]; - const matrix = localToVpMatrix.clone(); - matrix.scale(sx, sy, vpFixedPivot); - matrix.append(localToVpMatrix.inverted()); - path.matrix = matrix; - console.log(item.data.id); - const newPl = newVl.findLayerById(item.data.id).clone() as MorphableLayer; - newPl.pathData = new Path(path.pathData); - newVl = LayerUtil.replaceLayer(newVl, item.data.id, newPl); - }); - - return newVl; - } - - // TODO: reuse this code with SelectDragCloneItemsGesture - private buildSnapInfo() { - const selectedLayerIds = this.ps.getSelectedLayerIds(); - if (!selectedLayerIds.size) { - return undefined; - } - const draggedItems = Array.from(selectedLayerIds).map(id => this.pl.findItemByLayerId(id)); - const { parent } = draggedItems[0]; - if (!draggedItems.every(item => item.parent === parent)) { - // TODO: copy the behavior used in Sketch - console.warn('All snapped items must share the same parent item.'); - return undefined; - } - const siblingItems = parent.children.filter(i => !draggedItems.includes(i)); - if (!siblingItems.length) { - return undefined; - } - - // Perform the snap test. - const toSnapPointsFn = (items: ReadonlyArray) => { - const { topLeft, center, bottomRight } = PaperUtil.computeBounds(items); - return [topLeft, center, bottomRight]; - }; - // TODO: also snap-to-VectorLayer bounds (similar to the dragging gesture) - return SnapUtil.computeSnapInfo( - toSnapPointsFn(draggedItems), - siblingItems.map(siblingItem => toSnapPointsFn([siblingItem])), - true /* snapToDimensions */, - ); - } - - private projToVpLine({ from, to }: Line) { - return { - from: this.pl.globalToLocal(new paper.Point(from)), - to: this.pl.globalToLocal(new paper.Point(to)), - }; - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/scale/index.ts b/src/app/pages/editor/scripts/paper/gesture/scale/index.ts deleted file mode 100644 index 759e4d53..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/scale/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ScaleItemsGesture } from './ScaleItemsGesture'; diff --git a/src/app/pages/editor/scripts/paper/gesture/select/BatchSelectItemsGesture.ts b/src/app/pages/editor/scripts/paper/gesture/select/BatchSelectItemsGesture.ts deleted file mode 100644 index 40307312..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/select/BatchSelectItemsGesture.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -/** - * A gesture that selects one or more items using a selection box. - * - * Preconditions: - * - The user is in default mode. - */ -export class BatchSelectItemsGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - // private initialSelectedLayers: ReadonlySet; - - constructor(private readonly ps: PaperService) { - super(); - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - if (!event.modifiers.shift) { - // A selection box implies that the gesture began with a failed hit test, - // so deselect everything on mouse down (unless the user is holding shift). - this.ps.setSelectedLayerIds(new Set()); - } - // TODO: make use of this information (i.e. toggle the layers when shift is pressed) - // this.initialSelectedLayers = this.ps.getSelectedLayerIds(); - } - - // @Override - onMouseDrag(event: paper.ToolEvent) { - this.ps.setSelectionBox({ - from: this.pl.globalToLocal(event.downPoint), - to: this.pl.globalToLocal(event.point), - }); - this.selectItemsInSelectionBox(!event.modifiers.alt); - } - - // @Override - onMouseUp(event: paper.ToolEvent) { - this.selectItemsInSelectionBox(!event.modifiers.alt); - this.ps.setSelectionBox(undefined); - this.ps.setRotateItemsInfo(undefined); - this.ps.setTransformPathsInfo(undefined); - } - - // @Override - onKeyDown(event: paper.KeyEvent) { - if (event.key === 'alt') { - this.selectItemsInSelectionBox(false); - } - } - - // @Override - onKeyUp(event: paper.KeyEvent) { - if (event.key === 'alt') { - this.selectItemsInSelectionBox(true); - } - } - - private selectItemsInSelectionBox(includePartialOverlaps: boolean) { - const box = this.ps.getSelectionBox(); - if (box) { - const from = new paper.Point(box.from); - const to = new paper.Point(box.to); - const selectedItems = this.pl.findItemsInBounds( - new paper.Rectangle(from, to), - includePartialOverlaps, - ); - this.ps.setSelectedLayerIds(new Set(selectedItems.map(i => i.data.id))); - } - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/select/DeselectItemGesture.ts b/src/app/pages/editor/scripts/paper/gesture/select/DeselectItemGesture.ts deleted file mode 100644 index d65d62b7..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/select/DeselectItemGesture.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -/** - * A gesture that deselects a single item. - * - * Preconditions: - * - The user is in default mode. - */ -export class DeselectItemGesture extends Gesture { - constructor(private readonly ps: PaperService, private readonly deselectedItemId: string) { - super(); - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - const layerIds = new Set(this.ps.getSelectedLayerIds()); - layerIds.delete(this.deselectedItemId); - this.ps.setSelectedLayerIds(layerIds); - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/select/EditPathGesture.ts b/src/app/pages/editor/scripts/paper/gesture/select/EditPathGesture.ts deleted file mode 100644 index 8992f2c1..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/select/EditPathGesture.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -/** - * A gesture that exits default mode and enters edit path mode. - * - * Preconditions: - * - The user is in default mode. - */ -export class EditPathGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - - constructor(private readonly ps: PaperService, private readonly editPathId: string) { - super(); - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - const editPath = this.pl.findItemByLayerId(this.editPathId) as paper.Path; - this.ps.setSelectedLayerIds(new Set([this.editPathId])); - this.ps.setEditPathInfo({ - ...PaperUtil.selectCurves(editPath, new Set([editPath.segments.length - 1])), - }); - this.ps.setRotateItemsInfo(undefined); - this.ps.setTransformPathsInfo(undefined); - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/select/SelectDragCloneItemsGesture.ts b/src/app/pages/editor/scripts/paper/gesture/select/SelectDragCloneItemsGesture.ts deleted file mode 100644 index 0cb497a2..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/select/SelectDragCloneItemsGesture.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { - ClipPathLayer, - GroupLayer, - LayerUtil, - PathLayer, - VectorLayer, -} from 'app/pages/editor/model/layers'; -import { CursorType } from 'app/pages/editor/model/paper'; -import { MathUtil, Matrix } from 'app/pages/editor/scripts/common'; -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil, SnapUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import { Line } from 'app/pages/editor/store/paper/actions'; -import * as paper from 'paper'; - -/** - * A gesture that performs selection, move, and clone operations - * on one or more items. - * - * Preconditions: - * - The user is in default mode. - * - The user hit an item in the previous mousedown event. - */ -export class SelectDragCloneItemsGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - private initialVectorLayer: VectorLayer; - private isDragging = false; - - constructor(private readonly ps: PaperService, private readonly hitLayerId: string) { - super(); - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - // Clear the current hover layer, if it exists. - this.ps.setHoveredLayerId(undefined); - - const selectedLayers = new Set(this.ps.getSelectedLayerIds()); - if (!event.modifiers.shift && !selectedLayers.has(this.hitLayerId)) { - // If shift isn't pressed and the hit layer isn't already selected, - // then clear any existing selections. - selectedLayers.clear(); - } - - // Select the hit item. - selectedLayers.add(this.hitLayerId); - this.ps.setSelectedLayerIds(selectedLayers); - - // Save a copy of the initial vector layer so that we can make changes - // to it as we drag. - this.initialVectorLayer = this.ps.getVectorLayer(); - } - - // @Override - onMouseDrag(event: paper.ToolEvent) { - if (!this.isDragging) { - this.isDragging = true; - if (event.modifiers.alt) { - // TODO: clone the selected items - } - this.ps.setCursorType(CursorType.Grabbing); - } - - let newVl = this.initialVectorLayer.clone(); - newVl = this.dragItems(newVl, event.downPoint, event.point, event.modifiers.shift); - this.ps.setVectorLayer(newVl); - - // TODO: this could be WAY more efficient (no need to drag/snap things twice) - const snapInfo = this.buildSnapInfo(); - if (snapInfo) { - const projSnapDelta = new paper.Point(snapInfo.projSnapDelta); - if (!projSnapDelta.isZero()) { - newVl = this.dragItems(newVl, event.downPoint, event.downPoint.add(projSnapDelta)); - this.ps.setVectorLayer(newVl); - } - const updatedSnapInfo = this.buildSnapInfo(); - this.ps.setSnapGuideInfo({ - guides: updatedSnapInfo.guides.map(g => this.projToVpLine(g)), - rulers: updatedSnapInfo.rulers.map(r => this.projToVpLine(r)), - }); - } else { - this.ps.setSnapGuideInfo(undefined); - } - } - - // TODO: dragging a parent and child simultaneously doesn't work - private dragItems( - newVl: VectorLayer, - projDownPoint: paper.Point, - projPoint: paper.Point, - shouldSnapDelta = false, - ) { - Array.from(this.ps.getSelectedLayerIds()).forEach(layerId => { - const item = this.pl.findItemByLayerId(layerId); - const localDown = item.globalToLocal(projDownPoint).transform(item.matrix); - const localCurr = item.globalToLocal(projPoint).transform(item.matrix); - const localDelta = localCurr.subtract(localDown); - const localFinalDelta = shouldSnapDelta - ? new paper.Point(MathUtil.snapVectorToAngle(localDelta, 90)) - : localDelta; - newVl = dragItem(newVl, layerId, localFinalDelta); - }); - return newVl; - } - - // TODO: reuse this code with ScaleItemsGesture - private buildSnapInfo() { - const selectedLayerIds = this.ps.getSelectedLayerIds(); - if (!selectedLayerIds.size) { - return undefined; - } - const draggedItems = Array.from(selectedLayerIds).map(id => this.pl.findItemByLayerId(id)); - const { parent } = draggedItems[0]; - if (!draggedItems.every(item => item.parent === parent)) { - // TODO: copy the behavior used in Sketch - console.warn('All snapped items must share the same parent item.'); - return undefined; - } - const siblingItems = parent.children.filter(i => !draggedItems.includes(i)); - const isParentVectorLayer = parent.data.id === this.initialVectorLayer.id; - if (!siblingItems.length && !isParentVectorLayer) { - return undefined; - } - - // Perform the snap test. - const siblingSnapPointsTable = siblingItems.map(item => toSnapPoints([item])); - if (isParentVectorLayer) { - const { width, height } = this.initialVectorLayer; - const topLeft = new paper.Point(0, 0); - const bottomRight = parent.localToGlobal(new paper.Point(width, height)); - const center = bottomRight.divide(2); - siblingSnapPointsTable.push([topLeft, center, bottomRight]); - } - return SnapUtil.computeSnapInfo(toSnapPoints(draggedItems), siblingSnapPointsTable); - } - - // @Override - onMouseUp(event: paper.ToolEvent) { - this.ps.setSnapGuideInfo(undefined); - this.ps.setCursorType(CursorType.Default); - } - - private projToVpLine({ from, to }: Line): Line { - return { - from: this.pl.globalToLocal(new paper.Point(from)), - to: this.pl.globalToLocal(new paper.Point(to)), - }; - } -} - -// TODO: should we bake transforms into children (to be consistent with scale items gesture?) -function dragItem(newVl: VectorLayer, layerId: string, localDelta: paper.Point) { - const initialLayer = newVl.findLayerById(layerId); - const { x, y } = localDelta; - if (initialLayer instanceof PathLayer || initialLayer instanceof ClipPathLayer) { - const replacementLayer = initialLayer.clone(); - replacementLayer.pathData = initialLayer.pathData.transform(Matrix.translation(x, y)); - newVl = LayerUtil.replaceLayer(newVl, layerId, replacementLayer); - } else if (initialLayer instanceof GroupLayer) { - const replacementLayer = initialLayer.clone(); - replacementLayer.translateX += x; - replacementLayer.translateY += y; - newVl = LayerUtil.replaceLayer(newVl, layerId, replacementLayer); - } - return newVl; -} - -function toSnapPoints(items: ReadonlyArray) { - const { topLeft, center, bottomRight } = PaperUtil.computeBounds(items); - return [topLeft, center, bottomRight]; -} diff --git a/src/app/pages/editor/scripts/paper/gesture/select/index.ts b/src/app/pages/editor/scripts/paper/gesture/select/index.ts deleted file mode 100644 index 4db4e28d..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/select/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { BatchSelectItemsGesture } from './BatchSelectItemsGesture'; -export { DeselectItemGesture } from './DeselectItemGesture'; -export { EditPathGesture } from './EditPathGesture'; -export { SelectDragCloneItemsGesture } from './SelectDragCloneItemsGesture'; diff --git a/src/app/pages/editor/scripts/paper/gesture/transform/TransformPathsGesture.ts b/src/app/pages/editor/scripts/paper/gesture/transform/TransformPathsGesture.ts deleted file mode 100644 index e5d902ae..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/transform/TransformPathsGesture.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { LayerUtil, PathLayer, VectorLayer } from 'app/pages/editor/model/layers'; -import { Path } from 'app/pages/editor/model/paths'; -import { TransformUtil } from 'app/pages/editor/scripts/common'; -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { PaperLayer, SelectionBoundsRaster } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -/** - * A gesture that performs transform operations. - * - * Preconditions: - * - The user is in default mode. - * - One or more paths are selected. - * - A mouse down event occurred on a selection bounds handle. - * - * TODO: finish this - * TODO: fix crash that can occur when 3+ points are on same axis - * TODO: could this work with generic items (not just paths)? - * TODO: we need to also filter out non-empty groups (see PaperLayer.ts) - */ -export class TransformPathsGesture extends Gesture { - private readonly pl = paper.project.activeLayer as PaperLayer; - private selectedItems: ReadonlyArray; - private localToVpItemMatrices: ReadonlyArray; - private initialVectorLayer: VectorLayer; - private vpDownPoint: paper.Point; - private vpPoint: paper.Point; - private vpBounds: paper.Rectangle; - - constructor( - private readonly ps: PaperService, - private readonly selectionBoundsRaster: SelectionBoundsRaster, - ) { - super(); - } - - // @Override - onMouseDown(event: paper.ToolEvent) { - this.ps.setHoveredLayerId(undefined); - this.selectedItems = Array.from(this.ps.getSelectedLayerIds()).map( - id => this.pl.findItemByLayerId(id) as paper.Path, - ); - const invertedPaperLayerMatrix = this.pl.matrix.inverted(); - this.localToVpItemMatrices = this.selectedItems.map(item => { - // Compute the matrices to directly transform during drag events. - return item.globalMatrix.prepended(invertedPaperLayerMatrix).inverted(); - }); - this.vpBounds = PaperUtil.transformRectangle( - PaperUtil.computeBounds(this.selectedItems), - this.pl.matrix.inverted(), - ); - this.vpDownPoint = this.vpBounds[this.selectionBoundsRaster.pivotType]; - this.vpPoint = this.vpDownPoint; - this.initialVectorLayer = this.ps.getVectorLayer(); - } - - // @Override - onMouseDrag(event: paper.ToolEvent) { - this.vpPoint = this.pl.globalToLocal(event.point); - this.processEvent(event); - } - - // @Override - onKeyDown(event: paper.KeyEvent) { - this.processKeyEvent(event); - } - - // @Override - onKeyUp(event: paper.KeyEvent) { - this.processKeyEvent(event); - } - - private processKeyEvent(event: paper.KeyEvent) { - if (event.key === 'command') { - this.processEvent(event); - } - } - - private processEvent(event: paper.Event) { - if (!this.vpPoint) { - return; - } - const sourcePoints = [ - this.vpBounds.topLeft, - this.vpBounds.topRight, - this.vpBounds.bottomRight, - this.vpBounds.bottomLeft, - ].map(({ x, y }) => [x, y] as [number, number]); - const targetPoints = [...sourcePoints]; - const vpPoint = [this.vpPoint.x, this.vpPoint.y] as [number, number]; - switch (this.selectionBoundsRaster.pivotType) { - case 'topLeft': - targetPoints[0] = vpPoint; - break; - case 'topRight': - targetPoints[1] = vpPoint; - break; - case 'bottomRight': - targetPoints[2] = vpPoint; - break; - case 'bottomLeft': - targetPoints[3] = vpPoint; - break; - } - - const distortFn = TransformUtil.distort(sourcePoints, targetPoints); - - let newVl = this.initialVectorLayer.clone(); - this.selectedItems.forEach((item, index) => { - // TODO: make this stuff works for groups as well - const path = item.clone() as paper.Path; - const localToViewportMatrix = this.localToVpItemMatrices[index]; - const pathDistortFn = (point: paper.Point) => { - point = localToViewportMatrix.transform(point); - const intermediatePoint = distortFn([point.x, point.y]); - point = new paper.Point(intermediatePoint[0], intermediatePoint[1]); - point = localToViewportMatrix.inverted().transform(point); - return point; - }; - path.segments.forEach(segment => { - if (segment.handleIn) { - segment.handleIn = pathDistortFn(segment.point.add(segment.handleIn)).subtract( - segment.point, - ); - } - if (segment.handleOut) { - segment.handleOut = pathDistortFn(segment.point.add(segment.handleOut)).subtract( - segment.point, - ); - } - segment.point = pathDistortFn(segment.point); - }); - const newPl = newVl.findLayerById(item.data.id).clone() as PathLayer; - newPl.pathData = new Path(path.pathData); - newVl = LayerUtil.replaceLayer(newVl, item.data.id, newPl); - }); - this.ps.setVectorLayer(newVl); - } -} diff --git a/src/app/pages/editor/scripts/paper/gesture/transform/index.ts b/src/app/pages/editor/scripts/paper/gesture/transform/index.ts deleted file mode 100644 index 4f5a7c5b..00000000 --- a/src/app/pages/editor/scripts/paper/gesture/transform/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TransformPathsGesture } from './TransformPathsGesture'; diff --git a/src/app/pages/editor/scripts/paper/index.ts b/src/app/pages/editor/scripts/paper/index.ts deleted file mode 100644 index 175f53e9..00000000 --- a/src/app/pages/editor/scripts/paper/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as paper from 'paper'; - -// By default paper.js bakes matrix transformations directly into its children. -// This is usually not the behavior we want (especially for groups). -paper.settings.applyMatrix = false; - -// By default paper.js automatically inserts newly created items into the active layer. -// This behavior makes it harder to explicitly position things in the item hierarchy. -paper.settings.insertItems = false; - -export { PaperProject } from './PaperProject'; diff --git a/src/app/pages/editor/scripts/paper/item/EditPathRaster.ts b/src/app/pages/editor/scripts/paper/item/EditPathRaster.ts deleted file mode 100644 index 7efc7be8..00000000 --- a/src/app/pages/editor/scripts/paper/item/EditPathRaster.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as paper from 'paper'; - -export class EditPathRaster extends paper.Raster { - constructor( - readonly type: 'segment' | 'handle-in' | 'handle-out', - readonly segmentIndex: number, - readonly isSelected: boolean, - center: paper.Point, - ) { - super( - type === 'segment' - ? `/assets/paper/${isSelected ? 'vector-segment-selected' : 'vector-segment'}.png` - : `/assets/paper/${isSelected ? 'vector-handle-selected' : 'vector-handle'}.png`, - center, - ); - } -} diff --git a/src/app/pages/editor/scripts/paper/item/HitTests.ts b/src/app/pages/editor/scripts/paper/item/HitTests.ts deleted file mode 100644 index 075c0151..00000000 --- a/src/app/pages/editor/scripts/paper/item/HitTests.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { Layer } from 'app/pages/editor/model/layers'; -import { PaperService } from 'app/pages/editor/services'; -import * as _ from 'lodash'; -import * as paper from 'paper'; - -import { EditPathRaster } from './EditPathRaster'; -import { HitResult, PaperLayer } from './PaperLayer'; -import { RotateItemsPivotRaster } from './RotateItemsPivotRaster'; -import { SelectionBoundsRaster } from './SelectionBoundsRaster'; - -/** Performs the default default mode hit test. */ -export function selectionMode(projPoint: paper.Point, ps: PaperService) { - const pl = paper.project.activeLayer as PaperLayer; - const { children } = pl.hitTestVectorLayer(projPoint); - const selectionMap = getSelectedLayerMap(ps); - return findFirstHitResult(children, selectionMap); -} - -/** - * Returns a map of layerIds to booleans. Each key-value pair indicates whether - * the subtree rooted at layerId contains a selected layer. - */ -export function getSelectedLayerMap(ps: PaperService) { - const map = new Map(); - const selectedLayers = ps.getSelectedLayerIds(); - (function containsSelectedLayerFn(layer: Layer) { - let result = selectedLayers.has(layer.id); - layer.children.forEach(c => (result = containsSelectedLayerFn(c) || result)); - map.set(layer.id, result); - return result; - })(ps.getVectorLayer()); - return map; -} - -export function findFirstHitResult( - hitResults: ReadonlyArray, - selectionMap: Map, - ignoredLayerIds = new Set(), -) { - let firstHitResult: HitResult; - _.forEach(hitResults, function recurseFn(hitResult: HitResult) { - if (firstHitResult) { - return false; - } - const hasSelectedChildLayer = (hitResult.hitItem.children || []).some(c => - selectionMap.get(c.data.id), - ); - if (!hasSelectedChildLayer && !ignoredLayerIds.has(hitResult.hitItem.data.id)) { - firstHitResult = hitResult; - return false; - } - _.forEach(hitResult.children, recurseFn); - return true; - }); - return firstHitResult; -} - -/** Performs a hit test on the currently selected selection bound handles. */ -export function selectionModeSegments(projPoint: paper.Point) { - const pl = paper.project.activeLayer as PaperLayer; - return pl.hitTest(projPoint, { class: SelectionBoundsRaster }); -} - -/** Performs a hit test on the rotate items pivot. */ -export function rotateItemsPivot(projPoint: paper.Point) { - const pl = paper.project.activeLayer as PaperLayer; - return pl.hitTest(projPoint, { class: RotateItemsPivotRaster }); -} - -/** Performs a hit test on the current edit path. */ -export function editPathMode( - projPoint: paper.Point, - editPath: paper.Path, - hitOptions: { fill?: boolean; stroke?: boolean; curves?: boolean }, -) { - const { x: sx, y: sy } = editPath.globalMatrix.scaling; - const result = editPath.hitTest(editPath.globalToLocal(projPoint), { - ...(hitOptions as paper.HitOptions), - // TODO: properly calculate scale using similar method as in Matrix.ts - // TODO: also test that this works when zoomed in/out? - // TODO: are we correctly handling negative scales? - tolerance: 8 / Math.max(Math.abs(sx), Math.abs(sy)), - class: paper.Path, - }); - return result; -} - -/** Performs a hit test on the current edit path's segments and handles. */ -export function editPathModeSegmentsAndHandles(projPoint: paper.Point) { - const pl = paper.project.activeLayer as PaperLayer; - return pl.hitTest(projPoint, { class: EditPathRaster }); -} diff --git a/src/app/pages/editor/scripts/paper/item/PaperLayer.ts b/src/app/pages/editor/scripts/paper/item/PaperLayer.ts deleted file mode 100644 index e0753465..00000000 --- a/src/app/pages/editor/scripts/paper/item/PaperLayer.ts +++ /dev/null @@ -1,779 +0,0 @@ -import { - ClipPathLayer, - GroupLayer, - Layer, - LayerUtil, - PathLayer, - VectorLayer, -} from 'app/pages/editor/model/layers'; -import { ColorUtil } from 'app/pages/editor/scripts/common'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import { - CreatePathInfo, - EditPathInfo, - SnapGuideInfo, - SplitCurveInfo, - TooltipInfo, -} from 'app/pages/editor/store/paper/actions'; -import * as _ from 'lodash'; -import * as paper from 'paper'; - -import { EditPathRaster } from './EditPathRaster'; -import { RotateItemsPivotRaster } from './RotateItemsPivotRaster'; -import { SelectionBoundsRaster } from './SelectionBoundsRaster'; - -/** - * The root layer used of our paper.js project. Note that this layer is - * assigned a scale matrix that converts global project coordinates to - * viewport coordinates. - * - * TODO: scaling rasters down causes their hit tolerances remain the same - * TODO: when multiple items selected, show lightly outlined bounds for individual items? - * TODO: explicitly set paths with no Z to closed? (i.e. M 1 1 h 6 v 6 h -6 v -6) - * TODO: figure out if we can reduce stable bundle sizes (tree shake paper.js?) - */ -export class PaperLayer extends paper.Layer { - private canvasColorRect: paper.Path.Rectangle; - private vectorLayerItem: paper.Item; - private selectionBoundsItem: paper.Item; - private rotateItemsPivotItem: paper.Item; - private hoverPathItem: paper.Path; - private selectionBoxItem: paper.Path; - private createPathItem: paper.Path; - private splitCurveItem: paper.Item; - private editPathItem: paper.Item; - private snapGuideItem: paper.Item; - private pixelGridItem: paper.Item; - private tooltipItem: paper.Item; - - private cssScaling = 1; - - constructor(private readonly ps: PaperService) { - super(); - this.canvasColorRect = new paper.Path.Rectangle(new paper.Point(0, 0), new paper.Size(0, 0)); - this.canvasColorRect.guide = true; - this.updateChildren(); - } - - private get vectorLayer() { - return this.ps.getVectorLayer(); - } - - private get selectedLayerIds() { - return this.ps.getSelectedLayerIds(); - } - - private get hoveredLayerId() { - return this.ps.getHoveredLayerId(); - } - - private get hiddenLayerIds() { - return this.ps.getHiddenLayerIds(); - } - - private get editPathInfo() { - return this.ps.getEditPathInfo(); - } - - private get rotateItemsInfo() { - return this.ps.getRotateItemsInfo(); - } - - hitTestVectorLayer(projPoint: paper.Point) { - const { width: vpWidth, height: vpHeight } = this.vectorLayer; - const vpBounds = new paper.Rectangle(0, 0, vpWidth, vpHeight); - const vpPoint = this.vectorLayerItem.globalToLocal(projPoint); - if (!vpBounds.contains(vpPoint)) { - return { hitItem: undefined, children: [] }; - } - const hitResult = (function recurseFn(item: paper.Item): HitResult { - const localPoint = item.globalToLocal(projPoint).transform(item.matrix); - let hitItem: paper.Item; - let children: HitResult[] = []; - if (item instanceof paper.Path) { - // TODO: figure out what to do with compound paths? - const res = item.hitTest(localPoint, { fill: true, stroke: true }); - if (res) { - hitItem = res.item; - } - } else if (item instanceof paper.Group) { - const { strokeBounds } = item; - if (strokeBounds.contains(localPoint)) { - hitItem = item; - children = item.children.map(recurseFn).filter(r => !!r.hitItem); - } - } - return { hitItem, children }; - })(this.vectorLayerItem); - return { - hitItem: this.vectorLayerItem, - children: hitResult.children, - }; - } - - setDimensions( - viewportWidth: number, - viewportHeight: number, - viewWidth: number, - viewHeight: number, - ) { - // Note that viewWidth / viewportWidth === viewHeight / viewportHeight. - this.cssScaling = viewWidth / viewportWidth; - this.matrix = new paper.Matrix().scale(this.cssScaling); - this.updatePixelGridItem(viewportWidth, viewportHeight); - } - - onVectorLayerChanged() { - this.updateCanvasColorShape(); - this.updateVectorLayerItem(); - this.updateEditPathItem(); - this.updateSelectionBoundsItem(); - this.updateRotateItemsPivotItem(); - this.updateHoverPathItem(); - } - - onSelectedLayerIdsChanged() { - this.updateSelectionBoundsItem(); - this.updateRotateItemsPivotItem(); - } - - onHiddenLayerIdsChanged() { - this.updateHiddenLayers(); - // TODO: should we hide selection bounds, overlays, etc. for invisible layers? - } - - onHoveredLayerIdChanged() { - this.updateHoverPathItem(); - } - - onEditPathInfoChanged() { - this.updateEditPathItem(); - this.updateSelectionBoundsItem(); - this.updateRotateItemsPivotItem(); - } - - onRotateItemsInfoChanged() { - this.updateRotateItemsPivotItem(); - } - - setCreatePathInfo(info: CreatePathInfo) { - if (this.createPathItem) { - this.createPathItem.remove(); - this.createPathItem = undefined; - } - if (info) { - this.createPathItem = newCreatePathItem(info); - this.updateChildren(); - } - } - - setSplitCurveInfo(info: SplitCurveInfo) { - if (this.splitCurveItem) { - this.splitCurveItem.remove(); - this.splitCurveItem = undefined; - } - if (info) { - this.splitCurveItem = newSplitCurveItem(info, this.cssScaling); - this.updateChildren(); - } - } - - setTooltipInfo(info: TooltipInfo) { - if (this.tooltipItem) { - this.tooltipItem.remove(); - this.tooltipItem = undefined; - } - if (info) { - // TODO: re-enable tooltip when ready - // this.tooltipItem = newTooltipItem(info, this.cssScaling); - this.updateChildren(); - } - } - - setSnapGuideInfo(info: SnapGuideInfo) { - if (this.snapGuideItem) { - this.snapGuideItem.remove(); - this.snapGuideItem = undefined; - } - if (info) { - this.snapGuideItem = newSnapGuideItem(info, this.cssScaling); - this.updateChildren(); - } - } - - setSelectionBox(box: { from: paper.Point; to: paper.Point }) { - if (this.selectionBoxItem) { - this.selectionBoxItem.remove(); - this.selectionBoxItem = undefined; - } - if (box) { - this.selectionBoxItem = newSelectionBoxItem(box.from, box.to); - this.updateChildren(); - } - } - - private updateCanvasColorShape() { - this.canvasColorRect = new paper.Path.Rectangle( - new paper.Point(0, 0), - new paper.Size(this.vectorLayer.width, this.vectorLayer.height), - ); - this.canvasColorRect.guide = true; - this.canvasColorRect.fillColor = parseAndroidColor(this.vectorLayer.canvasColor) || 'white'; - this.updateChildren(); - } - - private updateVectorLayerItem() { - if (this.vectorLayerItem) { - this.vectorLayerItem.remove(); - } - this.vectorLayerItem = newVectorLayerItem(this.vectorLayer); - this.updateHiddenLayers(); - this.updateChildren(); - } - - private updateSelectionBoundsItem() { - if (this.selectionBoundsItem) { - this.selectionBoundsItem.remove(); - this.selectionBoundsItem = undefined; - } - if (!this.editPathInfo) { - const selectedItemBounds = this.getSelectedItemBounds(); - if (selectedItemBounds) { - this.selectionBoundsItem = newSelectionBoundsItem(selectedItemBounds, this.cssScaling); - } - } - this.updateChildren(); - } - - private updateRotateItemsPivotItem() { - if (this.rotateItemsPivotItem) { - this.rotateItemsPivotItem.remove(); - this.rotateItemsPivotItem = undefined; - } - const rii = this.rotateItemsInfo; - if (rii) { - const selectedItemBounds = this.getSelectedItemBounds(); - if (selectedItemBounds) { - const vpPivot = rii.pivot ? new paper.Point(rii.pivot) : selectedItemBounds.center; - this.rotateItemsPivotItem = newRotationPivotItem(vpPivot, this.cssScaling); - } - } - this.updateChildren(); - } - - /** - * Returns the bounds of the currently selected items in project coordinates. - * Empty groups will be filtered out. Returns undefined if there are no selected - * items left to compute. - */ - private getSelectedItemBounds() { - const selectedItems = Array.from(this.selectedLayerIds) - .map(id => this.findItemByLayerId(id)) - // Filter out any selected empty groups. - .filter(i => !(i instanceof paper.Group) || i.children.length); - if (selectedItems.length === 0) { - return undefined; - } - return PaperUtil.transformRectangle( - PaperUtil.computeBounds(selectedItems), - this.matrix.inverted(), - ); - } - - private updateHiddenLayers() { - const hiddenLayerIds = this.hiddenLayerIds; - (function recurseFn(item: paper.Item) { - item.visible = !hiddenLayerIds.has(item.data.id); - if (item.hasChildren()) { - item.children.forEach(recurseFn); - } - })(this.vectorLayerItem); - } - - private updateHoverPathItem() { - if (this.hoverPathItem) { - this.hoverPathItem.remove(); - this.hoverPathItem = undefined; - } - if (this.hoveredLayerId) { - const item = this.findItemByLayerId(this.hoveredLayerId); - this.hoverPathItem = newHoverPathItem(item); - } - this.updateChildren(); - } - - private updateEditPathItem() { - if (this.editPathItem) { - this.editPathItem.remove(); - this.editPathItem = undefined; - } - const epi = this.editPathInfo; - const selectedLayerIds = this.selectedLayerIds; - if (epi && selectedLayerIds.size) { - // TODO: is it possible for pathData to be undefined? - const path = this.findItemByLayerId(selectedLayerIds.values().next().value) as paper.Path; - this.editPathItem = newEditPathItem(path, epi, this.cssScaling); - this.updateChildren(); - } - } - - private updatePixelGridItem(viewportWidth: number, viewportHeight: number) { - if (this.pixelGridItem) { - this.pixelGridItem.remove(); - this.pixelGridItem = undefined; - } - if (this.cssScaling > 4) { - this.pixelGridItem = newPixelGridItem(viewportWidth, viewportHeight); - this.updateChildren(); - } - } - - private updateChildren() { - this.children = _.compact([ - this.canvasColorRect, - this.vectorLayerItem, - this.selectionBoundsItem, - this.rotateItemsPivotItem, - this.hoverPathItem, - this.createPathItem, - this.splitCurveItem, - this.editPathItem, - this.snapGuideItem, - this.selectionBoxItem, - this.pixelGridItem, - this.tooltipItem, - ]); - } - - /** Finds the vector layer item with the given layer ID. */ - findItemByLayerId(layerId: string) { - if (!layerId) { - return undefined; - } - if (this.vectorLayerItem.data.id === layerId) { - return this.vectorLayerItem; - } - return _.first( - this.vectorLayerItem.getItems({ - match: ({ data: { id } }) => layerId === id, - }), - ); - } - - /** - * Finds all vector layer items that overlap with the specified bounds. - * Note that the bounds must be in viewport coordinates. - * @param includePartialOverlaps iff true, include items that partially overlap the bounds - */ - findItemsInBounds(vpBounds: paper.Rectangle, includePartialOverlaps: boolean) { - return this.vectorLayerItem.getItems({ - // TODO: figure out how to deal with groups and compound paths - class: paper.Path, - overlapping: includePartialOverlaps ? vpBounds : undefined, - inside: includePartialOverlaps ? undefined : vpBounds, - }); - } -} - -function parseAndroidColor(androidColor: string, alpha = 1) { - const color = ColorUtil.parseAndroidColor(androidColor); - return color - ? new paper.Color(color.r / 255, color.g / 255, color.b / 255, (color.a / 255) * alpha) - : undefined; -} - -function newVectorLayerItem(vl: VectorLayer): paper.Item { - const item = new paper.Group(); - if (!vl) { - return item; - } - - const fromPathLayerFn = (layer: PathLayer) => { - const { fillColor, fillAlpha, strokeColor, strokeAlpha } = layer; - const { trimPathStart, trimPathEnd, trimPathOffset } = layer; - // TODO: make sure this works with compound paths as well (Android behavior is different) - const pathLength = layer.pathData ? layer.pathData.getPathLength() : 0; - const dashArray = pathLength - ? LayerUtil.toStrokeDashArray(trimPathStart, trimPathEnd, trimPathOffset, pathLength) - : undefined; - const dashOffset = pathLength - ? LayerUtil.toStrokeDashOffset(trimPathStart, trimPathEnd, trimPathOffset, pathLength) - : undefined; - // TODO: import a compound path instead - // Only paths with more than one command can be closed. - const closed = - layer.pathData && layer.pathData.isClosed() && layer.pathData.getCommands().length > 1; - return new paper.Path({ - data: { id: layer.id }, - pathData: layer.pathData ? layer.pathData.getPathString() : '', - fillColor: parseAndroidColor(fillColor, fillAlpha), - strokeColor: parseAndroidColor(strokeColor, strokeAlpha), - strokeWidth: layer.strokeWidth, - miterLimit: layer.strokeMiterLimit, - strokeJoin: layer.strokeLinejoin, - strokeCap: layer.strokeLinecap, - fillRule: layer.fillType === 'evenOdd' ? 'evenodd' : 'nonzero', - dashArray, - dashOffset, - closed, - }); - }; - - const fromClipPathLayerFn = (layer: ClipPathLayer) => { - const pathData = layer.pathData ? layer.pathData.getPathString() : ''; - // Only paths with more than one command can be closed. - const closed = - layer.pathData && layer.pathData.isClosed() && layer.pathData.getCommands().length > 1; - return new paper.Path({ - data: { id: layer.id }, - pathData, - clipMask: true, - closed, - }); - }; - - const fromGroupLayerFn = (layer: GroupLayer) => { - const { pivotX, pivotY, scaleX, scaleY, rotation, translateX, translateY } = layer; - const pivot = new paper.Matrix(1, 0, 0, 1, pivotX, pivotY); - const scale = new paper.Matrix(scaleX, 0, 0, scaleY, 0, 0); - const cosr = Math.cos((rotation * Math.PI) / 180); - const sinr = Math.sin((rotation * Math.PI) / 180); - const rotate = new paper.Matrix(cosr, sinr, -sinr, cosr, 0, 0); - const translate = new paper.Matrix(1, 0, 0, 1, translateX, translateY); - const matrix = new paper.Matrix() - .prepend(pivot.inverted()) - .prepend(scale) - .prepend(rotate) - .prepend(translate) - .prepend(pivot); - return new paper.Group({ data: { id: layer.id }, matrix }); - }; - - item.data.id = vl.id; - item.opacity = vl.alpha; - item.addChildren( - vl.children.map(function recurseFn(layer: Layer) { - if (layer instanceof PathLayer) { - // TODO: return a compound path instead - return fromPathLayerFn(layer); - } - if (layer instanceof ClipPathLayer) { - // TODO: return a compound path instead - return fromClipPathLayerFn(layer); - } - if (layer instanceof GroupLayer) { - const groupItem = fromGroupLayerFn(layer); - groupItem.addChildren(layer.children.map(l => recurseFn(l))); - return groupItem; - } - throw new TypeError('Unknown layer type: ' + layer); - }), - ); - return item; -} - -/** Creates a new hover path for the specified item. */ -function newHoverPathItem(item: paper.Item) { - let hoverPath: paper.Path; - if (item instanceof paper.Group) { - hoverPath = new paper.Path.Rectangle(item.bounds); - } else if (item instanceof paper.Path) { - hoverPath = new paper.Path(item.segments); - hoverPath.closed = item.closed; - } - if (hoverPath) { - hoverPath.strokeColor = '#009dec'; - hoverPath.guide = true; - hoverPath.strokeScaling = false; - hoverPath.strokeWidth = 2 / paper.view.zoom; - // Transform the hover path from local coordinates to viewport coordinates. - hoverPath.matrix = item.globalMatrix - .prepended(paper.project.activeLayer.matrix.inverted()) - .prepend(item.matrix.inverted()); - } - return hoverPath; -} - -// TODO: reuse this code with SelectionBoundsRaster, etc. -const PIVOT_TYPES: Readonly< - [ - 'topLeft', - 'topCenter', - 'topRight', - 'rightCenter', - 'bottomRight', - 'bottomCenter', - 'bottomLeft', - 'leftCenter' - ] -> = [ - 'topLeft', - 'topCenter', - 'topRight', - 'rightCenter', - 'bottomRight', - 'bottomCenter', - 'bottomLeft', - 'leftCenter', -]; - -/** - * Creates a new selection bounds item for the specified selected items. - */ -function newSelectionBoundsItem(bounds: paper.Rectangle, cssScaling: number) { - const group = new paper.Group(); - - // Draw an outline for the bounded box. - const outlinePath = new paper.Path.Rectangle(bounds); - outlinePath.strokeScaling = false; - outlinePath.strokeWidth = 2 / paper.view.zoom; - outlinePath.strokeColor = '#e8e8e8'; - outlinePath.guide = true; - group.addChild(outlinePath); - - // Create segments for the bounded box. - PIVOT_TYPES.forEach(pivotType => { - // TODO: avoid creating rasters in a loop like this - const center = bounds[pivotType]; - const handle = SelectionBoundsRaster.of(pivotType, center); - const scaleFactor = getRasterScaleFactor(cssScaling); - handle.scale(scaleFactor, scaleFactor); - group.addChild(handle); - }); - - return group; -} - -/** - * Creates a rotation pivot point at the specified position. - */ -function newRotationPivotItem(position: paper.Point, cssScaling: number) { - const pivot = RotateItemsPivotRaster.of(position); - const scaleFactor = getRasterScaleFactor(cssScaling); - pivot.scale(scaleFactor, scaleFactor); - return pivot; -} - -/** - * Creates the overlay decorations for the given edit path. - */ -function newEditPathItem(path: paper.Path, info: EditPathInfo, cssScaling: number) { - const group = new paper.Group(); - const scaleFactor = getRasterScaleFactor(cssScaling); - - const matrix = path.globalMatrix.prepended( - new paper.Matrix(1 / cssScaling, 0, 0, 1 / cssScaling, 0, 0), - ); - const addRasterFn = (raster: paper.Raster) => { - raster.scale(scaleFactor, scaleFactor); - group.addChild(raster); - return raster; - }; - const addLineFn = (from: paper.Point, to: paper.Point) => { - const line = new paper.Path.Line(from, to); - line.guide = true; - line.strokeColor = '#aaaaaa'; - line.strokeWidth = 1 / paper.view.zoom; - line.strokeScaling = false; - // line.transform(matrix); - group.addChild(line); - }; - const { - selectedSegments, - visibleHandleIns, - selectedHandleIn, - visibleHandleOuts, - selectedHandleOut, - } = info; - // TODO: avoid creating rasters in a loop like this - path.segments.forEach(({ point, handleIn, handleOut }, segmentIndex) => { - const center = point.transform(matrix); - if (handleIn && visibleHandleIns.has(segmentIndex)) { - handleIn = point.add(handleIn).transform(matrix); - addLineFn(center, handleIn); - addRasterFn( - new EditPathRaster('handle-in', segmentIndex, selectedHandleIn === segmentIndex, handleIn), - ); - } - if (handleOut && visibleHandleOuts.has(segmentIndex)) { - handleOut = point.add(handleOut).transform(matrix); - addLineFn(center, handleOut); - addRasterFn( - new EditPathRaster( - 'handle-out', - segmentIndex, - selectedHandleOut === segmentIndex, - handleOut, - ), - ); - } - addRasterFn( - new EditPathRaster('segment', segmentIndex, selectedSegments.has(segmentIndex), center), - ); - }); - return group; -} - -function getRasterScaleFactor(cssScaling: number) { - return 1 / (1.8 * cssScaling * paper.view.zoom); -} - -function newCreatePathItem(info: CreatePathInfo) { - const path = new paper.Path(info.pathData); - path.guide = true; - path.strokeScaling = false; - path.strokeWidth = 1 / paper.view.zoom; - path.strokeColor = info.strokeColor; - return path; -} - -function newSplitCurveItem(info: SplitCurveInfo, cssScaling: number) { - const group = new paper.Group(); - group.guide = true; - - const { splitPoint, segment1, segment2 } = info; - const point1 = new paper.Point(segment1.point); - const handleIn1 = new paper.Point(segment1.handleIn); - const handleOut1 = new paper.Point(segment1.handleOut); - const point2 = new paper.Point(segment2.point); - const handleIn2 = new paper.Point(segment2.handleIn); - const handleOut2 = new paper.Point(segment2.handleOut); - const highlightedCurve = new paper.Path([ - new paper.Segment(point1, handleIn1, handleOut1), - new paper.Segment(point2, handleIn2, handleOut2), - ]); - highlightedCurve.guide = true; - highlightedCurve.strokeColor = '#3466A9'; - highlightedCurve.strokeScaling = false; - highlightedCurve.strokeWidth = 2 / paper.view.zoom; - group.addChild(highlightedCurve); - - const highlightedPoint = new paper.Path.Circle( - new paper.Point(splitPoint), - 4 / paper.view.zoom / cssScaling, - ); - highlightedPoint.guide = true; - highlightedPoint.fillColor = '#3466A9'; - group.addChild(highlightedPoint); - - return group; -} - -// TODO: add rounded rect background for tooltip -// TODO: ensure tooltip is justified correctly w/ respect to the active item -// function newTooltipItem(info: TooltipInfo, cssScaling: number) { -// return new paper.PointText({ -// point: info.point, -// content: info.label, -// fillColor: 'red', -// justification: 'left', -// // TODO: text doesn't display when using font size of only 12? -// fontSize: 14 / paper.view.zoom / cssScaling, -// fontFamily: 'Roboto, Helvetica Neue, sans-serif', -// guide: true, -// }); -// } - -function newSnapGuideItem(info: SnapGuideInfo, cssScaling: number) { - const group = new paper.Group({ guide: true }); - - const newLineFn = (from: paper.Point, to: paper.Point) => { - const line = new paper.Path.Line(from, to); - line.guide = true; - line.strokeScaling = false; - line.strokeWidth = 1 / paper.view.zoom; - line.strokeColor = 'red'; - return line; - }; - - info.guides.forEach(({ from, to }) => { - group.addChild(newLineFn(new paper.Point(from), new paper.Point(to))); - }); - - const addToAngleFn = (point: paper.Point, angle: number) => { - point = point.clone(); - point.angle += angle; - return point; - }; - - const newHandleLineFn = (endPoint: paper.Point, handle: paper.Point) => { - const from = endPoint.add(addToAngleFn(handle, 90)); - const to = endPoint.add(addToAngleFn(handle, -90)); - return newLineFn(from, to); - }; - - const newRulerLabelFn = (point: paper.Point, content: string) => { - // TODO: use a better font (roboto?) - // TODO: add padding above/to the side of the label - return new paper.PointText({ - point, - content, - fillColor: 'red', - // TODO: add justification so it displays to the bottom-left of the current point - fontSize: 12 / paper.view.zoom / cssScaling, - guide: true, - }); - }; - - const handleLengthPixels = 4; - const matrix = new paper.Matrix(cssScaling, 0, 0, cssScaling, 0, 0); - info.rulers.forEach(line => { - const from = new paper.Point(line.from); - const to = new paper.Point(line.to); - const mid = from.add(to.subtract(from).multiply(0.5)); - const globalFrom = from.transform(matrix); - const globalTo = to.transform(matrix); - const rulerHandle = globalTo - .subtract(globalFrom) - .normalize() - .multiply(handleLengthPixels) - .transform(matrix.inverted()); - // TODO: make sure the rounded vs. actual values are equal! - // TODO: only display decimals for small viewports - const pointTextLabel = _.round(from.getDistance(to), 1).toString(); - group.addChildren([ - newLineFn(from, to), - newHandleLineFn(from, rulerHandle), - newHandleLineFn(to, rulerHandle), - newRulerLabelFn(mid, pointTextLabel), - ]); - }); - - return group; -} - -function newSelectionBoxItem(from: paper.Point, to: paper.Point) { - const path = new paper.Path.Rectangle(new paper.Rectangle(from, to)); - path.guide = true; - path.strokeScaling = false; - path.strokeWidth = 1 / paper.view.zoom; - path.strokeColor = '#aaaaaa'; - path.dashArray = [3 / paper.view.zoom]; - return path; -} - -function newPixelGridItem(viewportWidth: number, viewportHeight: number) { - const group = new paper.Group({ guide: true }); - const newLineFn = (from: paper.Point, to: paper.Point) => { - const line = new paper.Path.Rectangle(from, to); - line.strokeColor = '#808080'; - line.opacity = 0.25; - line.strokeScaling = false; - line.strokeWidth = 1; - line.guide = true; - return line; - }; - for (let x = 1; x < viewportWidth; x++) { - group.addChild(newLineFn(new paper.Point(x, 0), new paper.Point(x, viewportHeight))); - } - for (let y = 1; y < viewportHeight; y++) { - group.addChild(newLineFn(new paper.Point(0, y), new paper.Point(viewportWidth, y))); - } - return group; -} - -export interface HitResult { - hitItem: paper.Item | undefined; - children: ReadonlyArray; -} diff --git a/src/app/pages/editor/scripts/paper/item/RotateItemsPivotRaster.ts b/src/app/pages/editor/scripts/paper/item/RotateItemsPivotRaster.ts deleted file mode 100644 index afc388ef..00000000 --- a/src/app/pages/editor/scripts/paper/item/RotateItemsPivotRaster.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as paper from 'paper'; - -export class RotateItemsPivotRaster extends paper.Raster { - private static instance: RotateItemsPivotRaster; - - static of(position: paper.Point) { - if (!RotateItemsPivotRaster.instance) { - RotateItemsPivotRaster.instance = new RotateItemsPivotRaster(); - } - const raster = RotateItemsPivotRaster.instance.clone(false) as RotateItemsPivotRaster; - raster.position = position; - return raster; - } - - constructor() { - super('/assets/paper/rotate-items-pivot.png'); - } -} diff --git a/src/app/pages/editor/scripts/paper/item/SelectionBoundsRaster.ts b/src/app/pages/editor/scripts/paper/item/SelectionBoundsRaster.ts deleted file mode 100644 index 4cfa44c2..00000000 --- a/src/app/pages/editor/scripts/paper/item/SelectionBoundsRaster.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { PivotType } from 'app/pages/editor/scripts/paper/util'; -import * as paper from 'paper'; - -const PIVOT_TYPES: ReadonlyArray = [ - 'bottomLeft', - 'leftCenter', - 'topLeft', - 'topCenter', - 'topRight', - 'rightCenter', - 'bottomRight', - 'bottomCenter', -]; - -export class SelectionBoundsRaster extends paper.Raster { - private static instance: SelectionBoundsRaster; - private pivotType_: PivotType; - private oppositePivotType_: PivotType; - - static of(pivotType: PivotType, center: paper.Point) { - if (!SelectionBoundsRaster.instance) { - SelectionBoundsRaster.instance = new SelectionBoundsRaster(); - } - const raster = SelectionBoundsRaster.instance.clone(false) as SelectionBoundsRaster; - raster.position = center; - raster.pivotType_ = pivotType; - raster.oppositePivotType_ = getOppositePivotType(pivotType); - return raster; - } - - constructor() { - super(`/assets/paper/selection-bounds-segment.png`); - } - - get pivotType() { - return this.pivotType_; - } - - get oppositePivotType() { - return this.oppositePivotType_; - } -} - -const OPPOSITE_PIVOT_TYPES: ReadonlyArray = ((arr: ReadonlyArray) => - arr.map((_, i) => arr[(i + arr.length / 2) % arr.length]))(PIVOT_TYPES); - -function getOppositePivotType(pivotType: PivotType) { - return OPPOSITE_PIVOT_TYPES[PIVOT_TYPES.indexOf(pivotType)]; -} diff --git a/src/app/pages/editor/scripts/paper/item/index.ts b/src/app/pages/editor/scripts/paper/item/index.ts deleted file mode 100644 index ab65e661..00000000 --- a/src/app/pages/editor/scripts/paper/item/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import * as HitTests from './HitTests'; -export { HitTests }; - -export { PaperLayer, HitResult } from './PaperLayer'; -export { SelectionBoundsRaster } from './SelectionBoundsRaster'; diff --git a/src/app/pages/editor/scripts/paper/tool/GestureTool.ts b/src/app/pages/editor/scripts/paper/tool/GestureTool.ts deleted file mode 100644 index 2e1c25f9..00000000 --- a/src/app/pages/editor/scripts/paper/tool/GestureTool.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { ToolMode } from 'app/pages/editor/model/paper'; -import { ClickDetector } from 'app/pages/editor/scripts/paper/detector'; -import { Gesture } from 'app/pages/editor/scripts/paper/gesture'; -import { - EllipseGesture, - PencilGesture, - RectangleGesture, -} from 'app/pages/editor/scripts/paper/gesture/create'; -import { - BatchSelectSegmentsGesture, - MouldCurveGesture, - SelectDragDrawSegmentsGesture, - SelectDragHandleGesture, - ToggleSegmentHandlesGesture, -} from 'app/pages/editor/scripts/paper/gesture/edit'; -import { HoverGesture } from 'app/pages/editor/scripts/paper/gesture/hover'; -import { - RotateItemsDragPivotGesture, - RotateItemsGesture, -} from 'app/pages/editor/scripts/paper/gesture/rotate'; -import { ScaleItemsGesture } from 'app/pages/editor/scripts/paper/gesture/scale'; -import { - BatchSelectItemsGesture, - DeselectItemGesture, - EditPathGesture, - SelectDragCloneItemsGesture, -} from 'app/pages/editor/scripts/paper/gesture/select'; -import { TransformPathsGesture } from 'app/pages/editor/scripts/paper/gesture/transform'; -import { HitTests, PaperLayer } from 'app/pages/editor/scripts/paper/item'; -import { PaperUtil } from 'app/pages/editor/scripts/paper/util'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -import { Tool } from './Tool'; - -/** - * A tool that delegates responsibilities to different gestures given the - * state of the current tool mode and key/mouse events. - */ -export class GestureTool extends Tool { - private readonly pl = paper.project.activeLayer as PaperLayer; - private readonly clickDetector = new ClickDetector(); - private currentGesture: Gesture = new HoverGesture(this.ps); - - constructor(private readonly ps: PaperService) { - super(); - } - - // @Override - onToolEvent(event: paper.ToolEvent) { - this.clickDetector.onToolEvent(event); - if (event.type === 'mousedown') { - this.onMouseDown(event); - } else if (event.type === 'mousedrag') { - this.currentGesture.onMouseDrag(event); - } else if (event.type === 'mousemove') { - this.currentGesture.onMouseMove(event); - } else if (event.type === 'mouseup') { - this.onMouseUp(event); - } - } - - private onMouseDown(event: paper.ToolEvent) { - const toolMode = this.ps.getToolMode(); - if (toolMode === ToolMode.Ellipse) { - this.currentGesture = new EllipseGesture(this.ps); - } else if (toolMode === ToolMode.Rectangle) { - this.currentGesture = new RectangleGesture(this.ps); - } else if (toolMode === ToolMode.Pencil) { - this.currentGesture = new PencilGesture(this.ps); - } else { - this.currentGesture = this.createSelectionModeGesture(event); - } - this.currentGesture.onMouseDown(event); - } - - private onMouseUp(event: paper.ToolEvent) { - this.currentGesture.onMouseUp(event); - this.currentGesture = new HoverGesture(this.ps); - } - - private createSelectionModeGesture(event: paper.ToolEvent) { - if (this.ps.getEditPathInfo()) { - return this.createEditPathModeGesture(event); - } - const selectedLayerIds = this.ps.getSelectedLayerIds(); - if (selectedLayerIds.size) { - // First perform a hit test on the selection bound's segments. - const selectionBoundSegmentsHitResult = HitTests.selectionModeSegments(event.point); - const rii = this.ps.getRotateItemsInfo(); - if (selectionBoundSegmentsHitResult) { - // If the hit item is a selection bound segment, then perform - // a scale/rotate/transform gesture. - if (rii) { - return new RotateItemsGesture(this.ps); - } - if (this.ps.getTransformPathsInfo()) { - return new TransformPathsGesture(this.ps, selectionBoundSegmentsHitResult.item); - } - return new ScaleItemsGesture(this.ps, selectionBoundSegmentsHitResult.item); - } - if (rii) { - // Perform a hit test on the rotate items pivot. - const rotateItemsHitResult = HitTests.rotateItemsPivot(event.point); - if (rotateItemsHitResult) { - return new RotateItemsDragPivotGesture(this.ps); - } - } - } - - const hitResults = this.pl.hitTestVectorLayer(event.point); - const selectionMap = HitTests.getSelectedLayerMap(this.ps); - const hitResult = HitTests.findFirstHitResult(hitResults.children, selectionMap); - if (!hitResult) { - // If there is no hit item, then batch select items using a selection box. - return new BatchSelectItemsGesture(this.ps); - } - - const hitItemId = hitResult.hitItem.data.id; - if (this.clickDetector.isDoubleClick()) { - const hitLayer = this.ps.getVectorLayer().findLayerById(hitItemId); - if (hitLayer.children.length) { - const newHitResult = HitTests.findFirstHitResult( - hitResults.children, - selectionMap, - new Set([hitLayer.id]), - ); - if (newHitResult) { - return new SelectDragCloneItemsGesture(this.ps, newHitResult.hitItem.data.id); - } else { - return new BatchSelectItemsGesture(this.ps); - } - } else { - // If a double click event occurs on top of a hit item w/ no children, - // then enter edit path mode. - return new EditPathGesture(this.ps, hitItemId); - } - } - - if (selectedLayerIds.has(hitItemId) && event.modifiers.shift && selectedLayerIds.size > 1) { - // If the hit item is selected, shift is pressed, and there is at least - // one other selected item, then deselect the hit item. - - // TODO: After the item is deselected, it should still be possible - // to drag/clone any other selected items in subsequent mouse events - return new DeselectItemGesture(this.ps, hitItemId); - } - - // TODO: The actual behavior in Sketch is a bit more complicated. - // For example, (1) a cloned item will not be generated until the next - // onMouseDrag event, (2) on the next onMouseDrag event, the - // cloned item should be selected and the currently selected item should - // be deselected, (3) the user can cancel a clone operation mid-drag by - // pressing/unpressing alt (even if alt wasn't initially pressed in - // onMouseDown). - - // At this point we know that either (1) the hit item is not selected - // or (2) the hit item is selected, shift is not being pressed, and - // there is only one selected item. In both cases the hit item should - // end up being selected. If alt is being pressed, then we should - // clone the item as well. - return new SelectDragCloneItemsGesture(this.ps, hitItemId); - } - - private createEditPathModeGesture(event: paper.ToolEvent) { - const selectedLayerIds = this.ps.getSelectedLayerIds(); - let layerId = selectedLayerIds.size ? selectedLayerIds.values().next().value : ''; - let epi = this.ps.getEditPathInfo(); - if (!layerId) { - // Then the user has created the first segment of a new path, in which - // case we must create a new dummy path and bring it into focus. - const newPathLayer = PaperUtil.addPathToStore(this.ps, ''); - layerId = newPathLayer.id; - epi = { - selectedSegments: new Set(), - visibleHandleIns: new Set(), - visibleHandleOuts: new Set(), - selectedHandleIn: undefined, - selectedHandleOut: undefined, - }; - this.ps.setSelectedLayerIds(new Set([layerId])); - this.ps.setEditPathInfo(epi); - } - - const editPathId = layerId; - const editPath = this.pl.findItemByLayerId(editPathId) as paper.Path; - - // First, do a hit test on the edit path's segments and handles. - const segmentsAndHandlesHitResult = HitTests.editPathModeSegmentsAndHandles(event.point); - if (segmentsAndHandlesHitResult) { - const { segmentIndex, type } = segmentsAndHandlesHitResult.item; - if (type === 'handle-in' || type === 'handle-out') { - // If a mouse down event occurred on top of a handle, - // then select/drag the handle. - return new SelectDragHandleGesture(this.ps, editPathId, segmentIndex, type); - } - if (this.clickDetector.isDoubleClick()) { - // If a double click occurred on top of a segment, then toggle the segment's handles. - return new ToggleSegmentHandlesGesture(this.ps, editPathId, segmentIndex); - } - // If a mouse down event occurred on top of a segment, - // then select/drag the segment. - return SelectDragDrawSegmentsGesture.hitSegment(this.ps, editPathId, segmentIndex); - } - - // Second, do a hit test on the edit path itself. - let hitResult = HitTests.editPathMode(event.point, editPath, { - fill: true, - stroke: true, - curves: true, - }); - if (hitResult) { - if (hitResult.type !== 'curve') { - // TODO: is there a way to avoid a second hit test like this? - hitResult = HitTests.editPathMode(event.point, editPath, { - curves: true, - }); - } - if (hitResult && hitResult.type === 'curve') { - if (event.modifiers.command) { - // If the user is holding down command, then modify the curve - // by dragging it. - return new MouldCurveGesture(this.ps, editPathId, { - curveIndex: hitResult.location.index, - time: hitResult.location.time, - }); - } - // Add a segment to the curve. - return SelectDragDrawSegmentsGesture.hitCurve( - this.ps, - editPathId, - hitResult.location.index, - hitResult.location.time, - ); - } - // Note that we won't exit edit path mode on the next mouse up event - // (since the gesture began with a successful hit test). - return new BatchSelectSegmentsGesture( - this.ps, - editPathId, - false /* clearEditPathOnDraglessClick */, - ); - } - - if (!editPath.segments.length) { - // Then we are beginning to build a new path from scratch. - return SelectDragDrawSegmentsGesture.miss(this.ps, editPathId); - } - - if (!editPath.closed && epi.selectedSegments.size === 1) { - const selectedSegmentIndex = epi.selectedSegments.values().next().value; - if (selectedSegmentIndex === 0 || selectedSegmentIndex === editPath.segments.length - 1) { - // Then we are extending an existing open path with a single selected end point segment. - return SelectDragDrawSegmentsGesture.miss(this.ps, editPathId); - } - } - - // If there is no hit item and we are in edit path mode, then - // enter selection box mode for the edit path so we can - // batch select its segments. If no drag occurs, the gesture will - // exit edit path mode on the next mouse up event. - return new BatchSelectSegmentsGesture( - this.ps, - editPathId, - true /* clearEditPathOnDraglessClick */, - ); - } - - // @Override - onKeyEvent(event: paper.KeyEvent) { - if (event.type === 'keydown') { - this.currentGesture.onKeyDown(event); - } else if (event.type === 'keyup') { - this.currentGesture.onKeyUp(event); - } - } -} diff --git a/src/app/pages/editor/scripts/paper/tool/MasterToolPicker.ts b/src/app/pages/editor/scripts/paper/tool/MasterToolPicker.ts deleted file mode 100644 index 743c3468..00000000 --- a/src/app/pages/editor/scripts/paper/tool/MasterToolPicker.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { ToolMode } from 'app/pages/editor/model/paper'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -import { GestureTool } from './GestureTool'; -import { Tool } from './Tool'; -import { ZoomPanTool } from './ZoomPanTool'; - -/** - * The master tool that is in charge of dispatching mouse, key, - * and tool mode change events. - */ -export class MasterToolPicker { - private readonly paperTool = new paper.Tool(); - - constructor(private readonly ps: PaperService) { - const gestureTool = new GestureTool(ps); - const zoomPanTool = new ZoomPanTool(ps); - let currentTool: Tool; - - const onEventFn = (event: paper.ToolEvent | paper.KeyEvent) => { - const prevTool = currentTool; - currentTool = - this.ps.getToolMode() === ToolMode.ZoomPan || (event && event.modifiers.space) - ? zoomPanTool - : gestureTool; - if (prevTool !== currentTool) { - if (prevTool) { - prevTool.onDeactivate(); - } - currentTool.onActivate(); - } - if (event instanceof paper.ToolEvent) { - currentTool.onToolEvent(event); - } else { - // TODO: do a better job at clearing focus in the property input component - // (i.e. clear focus when the user selects something in the canvas or timeline). - if (document.activeElement.matches('input')) { - // Ignore key events when an input element has focus. - return; - } - if (event.key === 'backspace' || event.key === 'delete') { - // In case there's a JS error, never navigate away. - event.preventDefault(); - } - currentTool.onKeyEvent(event); - } - }; - - this.paperTool.on({ - mousedown: onEventFn, - mousedrag: onEventFn, - mousemove: onEventFn, - mouseup: onEventFn, - keydown: onEventFn, - keyup: onEventFn, - }); - } - - onToolModeChanged() { - // TODO: better way to set this? - this.paperTool.fixedDistance = this.ps.getToolMode() === ToolMode.Pencil ? 4 : undefined; - } -} diff --git a/src/app/pages/editor/scripts/paper/tool/Tool.ts b/src/app/pages/editor/scripts/paper/tool/Tool.ts deleted file mode 100644 index 69d04488..00000000 --- a/src/app/pages/editor/scripts/paper/tool/Tool.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as paper from 'paper'; - -/** Represents the base class for all tool types. */ -export abstract class Tool { - /** Called immediately after this tool has been activated. */ - onActivate() {} - - /** - * Called when this tool has received a tool event (i.e. mouse down, - * mouse drag, mouse move, mouse up). - */ - onToolEvent(event: paper.ToolEvent) {} - - /** Called when this tool has received a key event (i.e. key down, key up). */ - onKeyEvent(event: paper.KeyEvent) {} - - /** Called immediately after this tool has been deactivated. */ - onDeactivate() {} -} diff --git a/src/app/pages/editor/scripts/paper/tool/ZoomPanTool.ts b/src/app/pages/editor/scripts/paper/tool/ZoomPanTool.ts deleted file mode 100644 index 4eb2faa2..00000000 --- a/src/app/pages/editor/scripts/paper/tool/ZoomPanTool.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { CursorType } from 'app/pages/editor/model/paper'; -import { PaperService } from 'app/pages/editor/services'; -import * as _ from 'lodash'; -import * as paper from 'paper'; - -import { Tool } from './Tool'; - -/** A tool that enables zooming and panning in the canvas. */ -export class ZoomPanTool extends Tool { - // Keep track of the last known mouse point in view space coordinates. - private viewLastPoint: paper.Point; - private isDragging: boolean; - - constructor(private readonly ps: PaperService) { - super(); - } - - // @Override - onActivate() { - this.viewLastPoint = new paper.Point(0, 0); - this.isDragging = false; - this.ps.setCursorType(CursorType.ZoomIn); - } - - // @Override - onDeactivate() { - // TODO: set the cursor type to whatever it was before onActivate()? - this.ps.setCursorType(CursorType.Default); - } - - // @Override - onToolEvent(event: paper.ToolEvent) { - if (event.type === 'mousedown') { - this.onMouseDown(event); - } else if (event.type === 'mousedrag') { - this.onMouseDrag(event); - } else if (event.type === 'mouseup') { - this.onMouseUp(event); - } - } - - private onMouseDown(event: paper.ToolEvent) { - this.isDragging = false; - this.updateCursorType(event); - - if (event.modifiers.space) { - // If space is pressed, then grab/pan the canvas. We store the last known - // mouse point in view space coordinates (which means the top left corner - // of the canvas will always be (0, 0), no matter how much we've panned/zoomed - // so far). - this.viewLastPoint = paper.view.projectToView(event.point); - return; - } - - // Zoom out if alt is pressed, and zoom in otherwise. - const zoom = paper.view.zoom * (event.modifiers.alt ? 1 / 2 : 2); - const { x, y } = paper.view.projectToView(event.point).subtract(event.point.multiply(zoom)); - this.setZoomPanInfo(zoom, x, y); - } - - private onMouseDrag(event: paper.ToolEvent) { - this.isDragging = false; - this.updateCursorType(event); - - if (!event.modifiers.space) { - return; - } - - // In order to have coordinate changes not mess up the dragging, we need to - // convert coordinates to view space, and then back to project space after - // the view has been scrolled. - const projPoint = event.point; - const viewPoint = paper.view.projectToView(projPoint); - const { tx, ty } = paper.view.matrix - .clone() - .translate(projPoint.subtract(paper.view.viewToProject(this.viewLastPoint))); - this.setZoomPanInfo(paper.view.zoom, tx, ty); - this.viewLastPoint = viewPoint; - } - - private onMouseUp(event: paper.ToolEvent) { - this.isDragging = false; - this.updateCursorType(event); - } - - private setZoomPanInfo(zoom: number, tx: number, ty: number) { - const { width, height } = paper.view.viewSize; - zoom = _.clamp(zoom, 1, 256); - tx = _.clamp(tx, -width * (zoom - 1), 0); - ty = _.clamp(ty, -height * (zoom - 1), 0); - this.ps.setZoomPanInfo({ zoom, translation: { tx, ty } }); - } - - // @Override - onKeyEvent(event: paper.KeyEvent) { - if (event.key === 'space' || event.key === 'alt') { - this.updateCursorType(event); - } - } - - private updateCursorType(event: paper.Event) { - if (event.modifiers.space) { - this.ps.setCursorType(this.isDragging ? CursorType.Grabbing : CursorType.Grab); - } else { - this.ps.setCursorType(event.modifiers.alt ? CursorType.ZoomOut : CursorType.ZoomIn); - } - } -} diff --git a/src/app/pages/editor/scripts/paper/tool/index.ts b/src/app/pages/editor/scripts/paper/tool/index.ts deleted file mode 100644 index a7bf7788..00000000 --- a/src/app/pages/editor/scripts/paper/tool/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { MasterToolPicker } from './MasterToolPicker'; diff --git a/src/app/pages/editor/scripts/paper/util/PaperUtil.ts b/src/app/pages/editor/scripts/paper/util/PaperUtil.ts deleted file mode 100644 index f9520f83..00000000 --- a/src/app/pages/editor/scripts/paper/util/PaperUtil.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Layer, LayerUtil, PathLayer } from 'app/pages/editor/model/layers'; -import { Path } from 'app/pages/editor/model/paths'; -import { PaperService } from 'app/pages/editor/services'; -import * as paper from 'paper'; - -/** Adds a new path to the first level of the vector layer tree. */ -export function addPathToStore(ps: PaperService, pathData: string) { - const vl = ps.getVectorLayer().clone(); - const pl = new PathLayer({ - name: LayerUtil.getUniqueLayerName([vl], 'path'), - children: [] as Layer[], - pathData: new Path(pathData), - // TODO: make this customizable - fillColor: '#D8D8D8', - strokeColor: '#979797', - strokeWidth: 0.1, - }); - vl.children = [...vl.children, pl]; - ps.setVectorLayer(vl); - return pl; -} - -/** Returns the path data string for the specified path layer ID. */ -export function getPathFromStore(ps: PaperService, layerId: string) { - const vl = ps.getVectorLayer(); - const pl = vl.findLayerById(layerId).clone() as PathLayer; - return pl.pathData.getPathString(); -} - -/** Replaces an existing path in the vector layer tree. */ -export function replacePathInStore(ps: PaperService, layerId: string, pathData: string) { - ps.setVectorLayer(getReplacePathInStoreVectorLayer(ps, layerId, pathData)); -} - -export function getReplacePathInStoreVectorLayer( - ps: PaperService, - layerId: string, - pathData: string, -) { - const vl = ps.getVectorLayer(); - const pl = vl.findLayerById(layerId).clone() as PathLayer; - pl.pathData = new Path(pathData); - return LayerUtil.replaceLayer(vl, layerId, pl); -} - -/** Computes the selected curves associated with the given selected segment indices. */ -export function selectCurves(path: paper.Path, selectedSegments: ReadonlySet) { - const visibleHandleIns = new Set(selectedSegments); - const visibleHandleOuts = new Set(selectedSegments); - selectedSegments.forEach(segmentIndex => { - // Also display the out-handle for the previous segment - // and the in-handle for the next segment. - const { previous, next } = path.segments[segmentIndex]; - if (previous) { - visibleHandleOuts.add(previous.index); - } - if (next) { - visibleHandleIns.add(next.index); - } - }); - return { - selectedSegments, - visibleHandleIns, - visibleHandleOuts, - selectedHandleIn: undefined as number, - selectedHandleOut: undefined as number, - }; -} - -/** Returns a new matrix that has been transformed by the specified matrix m. */ -export function transformRectangle(rect: paper.Rectangle, m: paper.Matrix) { - return new paper.Rectangle(rect.topLeft.transform(m), rect.bottomRight.transform(m)); -} - -/** Computes the bounds for the specified items in global project coordinates. */ -export function computeBounds(arg: paper.Item | ReadonlyArray) { - const flattenedItems: paper.Item[] = []; - (Array.isArray(arg) ? arg : [arg]).forEach(function recurseFn(i: paper.Item) { - if (i.hasChildren()) { - i.children.forEach(c => recurseFn(c)); - } else { - flattenedItems.push(i); - } - }); - return flattenedItems - .map(item => transformRectangle(item.bounds, item.globalMatrix)) - .reduce((p, c) => p.unite(c)); -} diff --git a/src/app/pages/editor/scripts/paper/util/PivotType.ts b/src/app/pages/editor/scripts/paper/util/PivotType.ts deleted file mode 100644 index 3b8640c0..00000000 --- a/src/app/pages/editor/scripts/paper/util/PivotType.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type PivotType = - | 'bottomLeft' - | 'leftCenter' - | 'topLeft' - | 'topCenter' - | 'topRight' - | 'rightCenter' - | 'bottomRight' - | 'bottomCenter'; diff --git a/src/app/pages/editor/scripts/paper/util/index.ts b/src/app/pages/editor/scripts/paper/util/index.ts deleted file mode 100644 index 5dcd685f..00000000 --- a/src/app/pages/editor/scripts/paper/util/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as PaperUtil from './PaperUtil'; -export { PaperUtil }; -export { PivotType } from './PivotType'; -export { SnapUtil } from './snap'; diff --git a/src/app/pages/editor/scripts/paper/util/snap/Constants.ts b/src/app/pages/editor/scripts/paper/util/snap/Constants.ts deleted file mode 100644 index f56f04d6..00000000 --- a/src/app/pages/editor/scripts/paper/util/snap/Constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -type H = Readonly<{ dir: 'horizontal'; start: 'left'; end: 'right'; size: 'width'; coord: 'x' }>; -type V = Readonly<{ dir: 'vertical'; start: 'top'; end: 'bottom'; size: 'height'; coord: 'y' }>; - -export type Horiz = H & Record<'opp', V>; -export type Vert = V & Record<'opp', H>; - -const horiz: H = { dir: 'horizontal', start: 'left', end: 'right', size: 'width', coord: 'x' }; -const vert: V = { dir: 'vertical', start: 'top', end: 'bottom', size: 'height', coord: 'y' }; - -export type Direction = 'horizontal' | 'vertical'; -export const DIRECTIONS: ['horizontal', 'vertical'] = ['horizontal', 'vertical']; -export const CONSTANTS: Readonly<{ horizontal: Horiz; vertical: Vert }> = { - horizontal: { ...horiz, opp: vert }, - vertical: { ...vert, opp: horiz }, -}; diff --git a/src/app/pages/editor/scripts/paper/util/snap/SnapBounds.ts b/src/app/pages/editor/scripts/paper/util/snap/SnapBounds.ts deleted file mode 100644 index e0b90450..00000000 --- a/src/app/pages/editor/scripts/paper/util/snap/SnapBounds.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { MathUtil, Point } from 'app/pages/editor/scripts/common'; -import { Line } from 'app/pages/editor/store/paper/actions'; -import * as _ from 'lodash'; - -export class SnapBounds { - readonly snapPoints: ReadonlyArray; - readonly left: number; - readonly top: number; - readonly right: number; - readonly bottom: number; - readonly width: number; - readonly height: number; - - constructor(...snapPoints: Point[]) { - this.snapPoints = snapPoints; - this.left = _.minBy(snapPoints, p => p.x).x; - this.top = _.minBy(snapPoints, p => p.y).y; - this.right = _.maxBy(snapPoints, p => p.x).x; - this.bottom = _.maxBy(snapPoints, p => p.y).y; - this.width = this.right - this.left; - this.height = this.bottom - this.top; - } - - /** Computes the minimum distance between two snap bounds. */ - distance(sb: SnapBounds): Readonly<{ line: Line; dist: number }> { - const { left: l1, top: t1, right: r1, bottom: b1 } = this; - const { left: l2, top: t2, right: r2, bottom: b2 } = sb; - const left = r2 < l1; - const top = b1 < t2; - const right = r1 < l2; - const bottom = b2 < t1; - let line: Line; - if (top && left) { - line = { from: { x: l1, y: b1 }, to: { x: r2, y: t2 } }; - } else if (left && bottom) { - line = { from: { x: l1, y: t1 }, to: { x: r2, y: b2 } }; - } else if (bottom && right) { - line = { from: { x: r1, y: t1 }, to: { x: l2, y: b2 } }; - } else if (right && top) { - line = { from: { x: r1, y: b1 }, to: { x: l2, y: t2 } }; - } else if (left) { - line = { from: { x: r2, y: t1 }, to: { x: l1, y: t1 } }; - } else if (right) { - line = { from: { x: r1, y: t1 }, to: { x: l2, y: t1 } }; - } else if (bottom) { - line = { from: { x: l1, y: b2 }, to: { x: l1, y: t1 } }; - } else if (top) { - line = { from: { x: l1, y: b1 }, to: { x: l1, y: t2 } }; - } else { - // TODO: handle this case better? (it implies the bounds intersect) - line = { from: { x: l1, y: t1 }, to: { x: l1, y: t1 } }; - } - return { line, dist: MathUtil.distance(line.from, line.to) }; - } -} diff --git a/src/app/pages/editor/scripts/paper/util/snap/SnapUtil.ts b/src/app/pages/editor/scripts/paper/util/snap/SnapUtil.ts deleted file mode 100644 index 57bb8f76..00000000 --- a/src/app/pages/editor/scripts/paper/util/snap/SnapUtil.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { MathUtil, Point } from 'app/pages/editor/scripts/common'; -import { Line } from 'app/pages/editor/store/paper/actions'; -import * as _ from 'lodash'; - -import { CONSTANTS, DIRECTIONS, Direction } from './Constants'; -import { SnapBounds } from './SnapBounds'; - -// TODO: make sure to test things with different stroke width values! -const SNAP_TOLERANCE_PIXELS = 10; - -/** - * A helper function that snaps the given dragged snap points to each of its siblings. - * - * TODO: make it possible to create rulers optionally only if a flag is passed - */ -export function computeSnapInfo( - dragSnapPoints: ReadonlyArray, - siblingSnapPointsTable: ReadonlyTable, - snapToDimensions = false, -): SnapInfo { - const dsb = new SnapBounds(...dragSnapPoints); - const ssbs = siblingSnapPointsTable.map(pts => new SnapBounds(...pts)); - const { horizontal, vertical } = snapToSiblings(dsb, ssbs, snapToDimensions); - const isHorizontalHit = Math.abs(horizontal.delta) <= SNAP_TOLERANCE_PIXELS; - const horizontalDelta = isHorizontalHit ? horizontal.delta : Infinity; - const horizontalValues = isHorizontalHit ? horizontal.values : []; - const isVerticalHit = Math.abs(vertical.delta) <= SNAP_TOLERANCE_PIXELS; - const verticalDelta = isVerticalHit ? vertical.delta : Infinity; - const verticalValues = isVerticalHit ? vertical.values : []; - const snapInfo: SnapInfoInternal = { - horizontal: { delta: horizontalDelta, values: horizontalValues }, - vertical: { delta: verticalDelta, values: verticalValues }, - }; - const projSnapDelta = { - x: isFinite(horizontalDelta) ? -horizontalDelta : 0, - y: isFinite(verticalDelta) ? -verticalDelta : 0, - }; - return { - projSnapDelta, - guides: buildGuides(snapInfo), - rulers: buildRulers(snapInfo), - }; -} - -/** Snaps the dragged item to each of its sibling snap items. */ -function snapToSiblings( - dsb: SnapBounds, - ssbs: ReadonlyArray, - snapToDimensions: boolean, -) { - // Compute a list of sibling snap results, where each entry represents a snapping - // between two snap bounds in both directions. - const ssrs = ssbs.map(ssb => { - // For each direction, return an entry consisting of: - // - dsb: the drag snap bounds - // - ssb: the sibling snap bounds - // - delta: the minimum delta value that would snap the two bounds - // - values: a list of snap pairs that computed the above delta value - return { - horizontal: { dsb, ssb, ...runSnapTest(dsb, ssb, snapToDimensions, 'horizontal') }, - vertical: { dsb, ssb, ...runSnapTest(dsb, ssb, snapToDimensions, 'vertical') }, - }; - }); - interface SnapResultInDirection { - readonly dsb: SnapBounds; - readonly ssb: SnapBounds; - readonly values: ReadonlyArray; - } - return { - horizontal: filterByMinDelta(ssrs.map(r => r.horizontal)), - vertical: filterByMinDelta(ssrs.map(r => r.vertical)), - }; -} - -/** - * Represents a valid snapping of two SnapBounds objects. - * The properties are indices into the SnapBounds' list of snap points. - */ -interface SnapPair { - readonly dragIndex?: number; - readonly siblingIndex?: number; - readonly isDimensionSnap?: boolean; -} - -interface SnapInfoInDirectionInternal { - // The minimum delta value found. - readonly delta: number; - // The list of snaps with the above delta value. - readonly values: ReadonlyArray<{ - // The currently dragged item. - readonly dsb: SnapBounds; - // The sibling item to snap to. - readonly ssb: SnapBounds; - // The list of snap pairs with the above delta value. - readonly values: ReadonlyArray; - }>; -} - -// Note that 'horizontal' snaps calculate vertical guides and 'vertical' -// snaps calculate horizontal guides.. This is because 'horizontal' -// snaps depend on differences in horizontal 'x' values (and vice-versa). -interface SnapInfoInternal { - readonly horizontal: SnapInfoInDirectionInternal; - readonly vertical: SnapInfoInDirectionInternal; -} - -export interface SnapInfo { - readonly projSnapDelta: Point; - readonly guides: ReadonlyArray; - readonly rulers: ReadonlyArray; -} - -interface Delta { - readonly delta: number; -} - -interface Values { - readonly values: ReadonlyArray; -} - -/** - * Runs a snap test for two snap bounds. The return result consists of (1) the - * minimum delta value found, and (2) a list of the values that had the specified - * delta value. - */ -function runSnapTest( - dsb: SnapBounds, - ssb: SnapBounds, - snapToDimensions: boolean, - dir: T, -): Values & Delta { - const { coord } = CONSTANTS[dir]; - const snapPairResults: (SnapPair & Delta)[] = []; - if (snapToDimensions) { - const getSnapBoundsSize = (sb: SnapBounds) => { - const min = _.minBy(sb.snapPoints, p => p[coord])[coord]; - const max = _.maxBy(sb.snapPoints, p => p[coord])[coord]; - return max - min; - }; - const dsbSize = getSnapBoundsSize(dsb); - const ssbSize = getSnapBoundsSize(ssb); - // TODO: improve this snap pair API stuff? - snapPairResults.push({ - isDimensionSnap: true, - delta: MathUtil.round(dsbSize - ssbSize), - }); - } else { - dsb.snapPoints.forEach((dragPoint, dragIndex) => { - ssb.snapPoints.forEach((siblingPoint, siblingIndex) => { - const delta = MathUtil.round(dragPoint[coord] - siblingPoint[coord]); - snapPairResults.push({ dragIndex, siblingIndex, delta }); - }); - }); - } - - return filterByMinDelta(snapPairResults); -} - -/** - * Filters the array of items, keeping the smallest delta values and - * discarding the rest. - */ -function filterByMinDelta(list: (T & Delta)[]): Values & Delta { - if (!list.length) { - return undefined; - } - const { min, pos, neg } = list.reduce( - (prev, curr) => { - const info = { - min: Math.abs(curr.delta), - pos: curr.delta >= 0 ? [curr] : ([] as (T & Delta)[]), - neg: curr.delta >= 0 ? [] : ([curr] as (T & Delta)[]), - }; - if (prev.min === info.min) { - return { - min: prev.min, - pos: [...prev.pos, ...info.pos], - neg: [...prev.neg, ...info.neg], - }; - } - return prev.min < info.min ? prev : info; - }, - { min: Infinity, pos: [] as (T & Delta)[], neg: [] as (T & Delta)[] }, - ); - const isDeltaPositive = pos.length >= neg.length; - return { - delta: min * (isDeltaPositive ? 1 : -1), - values: isDeltaPositive ? pos : neg, - }; -} - -/** - * Builds the snap guides to draw given a snap info object. - * This function assumes the global project coordinate space. - */ -function buildGuides(snapInfo: SnapInfoInternal): ReadonlyArray { - const guides: Line[] = []; - DIRECTIONS.forEach(d => guides.push(...buildGuidesInDirection(snapInfo, d))); - return guides; -} - -function buildGuidesInDirection( - snapInfo: SnapInfoInternal, - dir: T, -): ReadonlyArray { - const guides: Line[] = []; - const { coord, opp } = CONSTANTS[dir]; - snapInfo[dir].values.forEach(({ dsb, ssb, values }) => { - const firstGuideSnap = _.find(values, ({ dragIndex, siblingIndex }) => { - return dragIndex >= 0 && siblingIndex >= 0; - }); - if (firstGuideSnap) { - const startMostBounds = dsb[opp.start] < ssb[opp.start] ? dsb : ssb; - const endMostBounds = dsb[opp.end] < ssb[opp.end] ? ssb : dsb; - const startGuide = startMostBounds[opp.start]; - const endGuide = endMostBounds[opp.end]; - const coordGuide = ssb.snapPoints[firstGuideSnap.siblingIndex][coord]; - if (dir === 'horizontal') { - const from = { x: coordGuide, y: startGuide }; - const to = { x: coordGuide, y: endGuide }; - guides.push({ from, to }); - } else { - const from = { x: startGuide, y: coordGuide }; - const to = { x: endGuide, y: coordGuide }; - guides.push({ from, to }); - } - } - }); - return guides; -} - -/** - * Builds the snap rulers to draw given a snap info object. - * This function assumes the global project coordinate space. - */ -function buildRulers(snapInfo: SnapInfoInternal, snapToDimensions = false): ReadonlyArray { - const rulers: Line[] = []; - DIRECTIONS.forEach(d => rulers.push(...buildRulersInDirection(snapInfo, snapToDimensions, d))); - return rulers; -} - -// TODO: make sure that only one ruler total is made for the drag bounds! -// TODO: make sure that only one ruler total is made for the drag bounds! -// TODO: make sure that only one ruler total is made for the drag bounds! -// TODO: make sure that only one ruler total is made for the drag bounds! -// TODO: make sure that only one ruler total is made for the drag bounds! -function buildRulersInDirection( - snapInfo: SnapInfoInternal, - snapToDimensions = false, - dir: T, -): ReadonlyArray { - const rulers: Line[] = []; - snapInfo[dir].values.forEach(({ dsb, ssb, values }) => { - const dimensionSnaps = values.filter(({ isDimensionSnap }) => isDimensionSnap); - if (dimensionSnaps.length) { - const createRulerFn = (sb: SnapBounds) => { - const from = { x: sb.left, y: sb.top }; - const to = { - x: dir === 'horizontal' ? sb.right : sb.left, - y: dir === 'horizontal' ? sb.top : sb.bottom, - }; - return { from, to }; - }; - rulers.push(createRulerFn(dsb)); - rulers.push(createRulerFn(ssb)); - } - }); - for (const { dsb, ssb, values } of snapInfo[dir].values) { - const { start, end, opp } = CONSTANTS[dir]; - const oppStartMostBounds = dsb[opp.start] < ssb[opp.start] ? dsb : ssb; - const oppEndMostBounds = dsb[opp.end] < ssb[opp.end] ? ssb : dsb; - const nonStartMostBounds = dsb[start] < ssb[start] ? ssb : dsb; - const nonEndMostBounds = dsb[end] < ssb[end] ? dsb : ssb; - const guideSnaps = values.filter(({ isDimensionSnap }) => !isDimensionSnap); - if (guideSnaps.length) { - const oppStartRuler = oppStartMostBounds[opp.end]; - const oppEndRuler = oppEndMostBounds[opp.start]; - const startRuler = nonStartMostBounds[start]; - const endRuler = nonEndMostBounds[end]; - // TODO: handle the horizontal 'rulerLeft === rulerRight' case like sketch does - // TODO: handle the vertical 'rulerTop === rulerBottom' case like sketch does - const coordRuler = startRuler + (endRuler - startRuler) * 0.5; - const from = { - x: dir === 'horizontal' ? coordRuler : oppStartRuler, - y: dir === 'horizontal' ? oppStartRuler : coordRuler, - }; - const to = { - x: dir === 'horizontal' ? coordRuler : oppEndRuler, - y: dir === 'horizontal' ? oppEndRuler : coordRuler, - }; - const guideDelta = to[opp.coord] - from[opp.coord]; - if (guideDelta > SNAP_TOLERANCE_PIXELS) { - // Don't show a ruler if the items have been snapped (we assume that if - // the delta values is less than the snap tolerance, then it would have - // previously been snapped in the canvas such that the delta is now 0). - rulers.push({ from, to }); - } - break; - } - } - type Distance = Readonly<{ sb1: SnapBounds; sb2: SnapBounds; line: Line; dist: number }>; - const minDistsDragToSibling: Distance[] = []; - const minDistsSiblingToSibling: Distance[] = []; - // TODO: make it clear that there should only be one dsb in the snap info object? - const dsbs = snapInfo[dir].values.map(({ dsb }) => dsb); - const ssbs = snapInfo[dir].values.map(({ ssb }) => ssb); - const numValues = snapInfo[dir].values.length; - for (let i = 0; i < numValues; i++) { - minDistsDragToSibling.push({ sb1: dsbs[i], sb2: ssbs[i], ...dsbs[i].distance(ssbs[i]) }); - } - for (let i = 0; i < numValues - 1; i++) { - let minSsb: SnapBounds; - let minLine: Line; - let minDist = Infinity; - for (let j = i + 1; j < numValues; j++) { - const { line, dist } = ssbs[i].distance(ssbs[j]); - const absDist = Math.abs(dist); - if (absDist < minDist) { - minSsb = ssbs[j]; - minLine = line; - minDist = absDist; - } - } - minDistsSiblingToSibling.push({ sb1: ssbs[i], sb2: minSsb, line: minLine, dist: minDist }); - } - const minDistDragToSibling = _.minBy(minDistsDragToSibling, d => d.dist); - const matchingMinDistsSiblingToSibling = minDistsSiblingToSibling.filter(d => { - return Math.abs(minDistDragToSibling.dist - d.dist) <= SNAP_TOLERANCE_PIXELS; - }); - if (matchingMinDistsSiblingToSibling.length) { - rulers.push(minDistDragToSibling.line); - rulers.push(...matchingMinDistsSiblingToSibling.map(d => d.line)); - } - return rulers; -} diff --git a/src/app/pages/editor/scripts/paper/util/snap/index.ts b/src/app/pages/editor/scripts/paper/util/snap/index.ts deleted file mode 100644 index 1eae907c..00000000 --- a/src/app/pages/editor/scripts/paper/util/snap/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -import * as SnapUtil from './SnapUtil'; -export { SnapUtil }; diff --git a/src/app/pages/editor/scripts/svgo/index.ts b/src/app/pages/editor/scripts/svgo/index.ts deleted file mode 100644 index 24b3c8f7..00000000 --- a/src/app/pages/editor/scripts/svgo/index.ts +++ /dev/null @@ -1,150 +0,0 @@ -// TODO: find all differences between the svgo fork and svgo master (and create custom plugins for them?) -// TODO: test indirectly by testing the creation of the vector layer -// TODO: use promises as well for vector layer conversion -// TODO: make sure error handling works properly w/ the new promise architecture -// TODO: re-enable no implicit any and/or no implicit nulls? - -import * as js2svg from 'svgo/lib/svgo/js2svg'; -import * as executePlugins from 'svgo/lib/svgo/plugins'; -import * as svg2js from 'svgo/lib/svgo/svg2js'; -import * as cleanupAttrs from 'svgo/plugins/cleanupAttrs'; -import * as cleanupIDs from 'svgo/plugins/cleanupIDs'; -import * as cleanupNumericValues from 'svgo/plugins/cleanupNumericValues'; -import * as collapseGroups from 'svgo/plugins/collapseGroups'; -import * as convertPathData from 'svgo/plugins/convertPathData'; -import * as convertShapeToPath from 'svgo/plugins/convertShapeToPath'; -import * as convertStyleToAttrs from 'svgo/plugins/convertStyleToAttrs'; -import * as convertTransform from 'svgo/plugins/convertTransform'; -import * as inlineStyles from 'svgo/plugins/inlineStyles'; -import * as mergePaths from 'svgo/plugins/mergePaths'; -import * as minifyStyles from 'svgo/plugins/minifyStyles'; -import * as moveElemsAttrsToGroup from 'svgo/plugins/moveElemsAttrsToGroup'; -import * as moveGroupAttrsToElems from 'svgo/plugins/moveGroupAttrsToElems'; -import * as removeComments from 'svgo/plugins/removeComments'; -import * as removeDesc from 'svgo/plugins/removeDesc'; -import * as removeDoctype from 'svgo/plugins/removeDoctype'; -import * as removeEditorsNSData from 'svgo/plugins/removeEditorsNSData'; -import * as removeEmptyAttrs from 'svgo/plugins/removeEmptyAttrs'; -import * as removeEmptyContainers from 'svgo/plugins/removeEmptyContainers'; -import * as removeEmptyText from 'svgo/plugins/removeEmptyText'; -import * as removeHiddenElems from 'svgo/plugins/removeHiddenElems'; -import * as removeMetadata from 'svgo/plugins/removeMetadata'; -import * as removeNonInheritableGroupAttrs from 'svgo/plugins/removeNonInheritableGroupAttrs'; -import * as removeRasterImages from 'svgo/plugins/removeRasterImages'; -import * as removeScriptElement from 'svgo/plugins/removeScriptElement'; -import * as removeStyleElement from 'svgo/plugins/removeStyleElement'; -import * as removeTitle from 'svgo/plugins/removeTitle'; -import * as removeUnknownsAndDefaults from 'svgo/plugins/removeUnknownsAndDefaults'; -import * as removeUselessDefs from 'svgo/plugins/removeUselessDefs'; -import * as removeUselessStrokeAndFill from 'svgo/plugins/removeUselessStrokeAndFill'; -import * as removeXMLProcInst from 'svgo/plugins/removeXMLProcInst'; - -// Custom plugins. -import { convertRoundedRectToPath } from './plugins/convertRoundedRectToPath'; -import { replaceUseElems } from './plugins/replaceUseElems'; - -// The complete list is available here: https://github.com/svg/svgo/blob/master/.svgo.yml -const pluginsData = { - removeDoctype, - removeXMLProcInst, - removeComments, - removeMetadata, - // removeXMLNS, - removeEditorsNSData, - cleanupAttrs, - inlineStyles, - minifyStyles, - convertStyleToAttrs, - cleanupIDs, - // prefixIds, - removeRasterImages, - removeUselessDefs, - replaceUseElems, - cleanupNumericValues, - // cleanupListOfValues, - // convertColors, - removeUnknownsAndDefaults, - removeNonInheritableGroupAttrs, - removeUselessStrokeAndFill, - // removeViewBox, - // cleanupEnableBackground, - removeHiddenElems, - removeEmptyText, - convertShapeToPath, - convertRoundedRectToPath, - moveElemsAttrsToGroup, - moveGroupAttrsToElems, - collapseGroups, - convertPathData, - convertTransform, - removeEmptyAttrs, - removeEmptyContainers, - mergePaths, - // removeUnusedNS, - // sortAttrs, - removeTitle, - removeDesc, - // removeDimensions - // removeAttrs, - // removeElementsByAttr, - // addClassesToSVGElement, - removeStyleElement, - removeScriptElement, - // addAttributesToSVGElement, -}; - -// Set a global floatPrecision across all the plugins. -const floatPrecision = 6; -for (const plugin of Object.values(pluginsData)) { - if (plugin.params && 'floatPrecision' in plugin.params) { - plugin.params.floatPrecision = floatPrecision; - } -} - -// Tweak plugin params. -cleanupIDs.params.minify = false; -convertPathData.params.makeArcs = undefined; -convertPathData.params.transformPrecision = floatPrecision; -convertShapeToPath.params.convertArcs = true; -convertTransform.params.transformPrecision = floatPrecision; -inlineStyles.params.onlyMatchedOnce = false; -removeUselessStrokeAndFill.params.removeNone = true; - -const optimizedPluginsData = (function () { - return Object.values(pluginsData) - .map(item => [item]) - .reduce((arr, item) => { - const last = arr[arr.length - 1]; - if (last && item[0].type === last[0].type) { - last.push(item[0]); - } else { - arr.push(item); - } - return arr; - }, []); -})(); - -export function optimizeSvg(svgText: string, pretty = true): Promise { - return new Promise((resolve, reject) => { - const callbackFn = (svgjs: any) => { - if (svgjs.error) { - reject(svgjs.error); - return; - } - resolve(svgjs.data); - }; - svg2js(svgText, (svgjs: any) => { - if (svgjs.error) { - callbackFn(svgjs); - return; - } - svgjs = executePlugins(svgjs, { input: 'string' }, optimizedPluginsData); - callbackFn( - js2svg(svgjs, { - indent: ' ', - pretty, - }), - ); - }); - }); -} diff --git a/src/app/pages/editor/scripts/svgo/plugins/convertRoundedRectToPath.ts b/src/app/pages/editor/scripts/svgo/plugins/convertRoundedRectToPath.ts deleted file mode 100644 index caaf6987..00000000 --- a/src/app/pages/editor/scripts/svgo/plugins/convertRoundedRectToPath.ts +++ /dev/null @@ -1,91 +0,0 @@ -export const convertRoundedRectToPath = { - active: true, - type: 'perItem', - fn: convertRoundedRectToPathFn, - params: undefined as any, -}; - -const none = { value: 0 }; - -/** - * Converts a rounded rect to a more compact path. - * It also allows further optimizations like - * combining paths with similar attributes. - * - * @see http://www.w3.org/TR/SVG/shapes.html - * - * @param {Object} item current iteration item - * @param {Object} params plugin params - * @return {Boolean} if false, item will be filtered out - */ -function convertRoundedRectToPathFn(item: any, params: any): any { - if ( - !item.isElem('rect') || - !item.hasAttr('width') || - !item.hasAttr('height') || - !(item.hasAttr('rx') || item.hasAttr('ry')) - ) { - return undefined; - } - - const x = +(item.attr('x') || none).value; - const y = +(item.attr('y') || none).value; - const width = +item.attr('width').value; - const height = +item.attr('height').value; - const hasRx = item.hasAttr('rx') && isValidCornerRadius(+item.attr('rx').value); - const hasRy = item.hasAttr('ry') && isValidCornerRadius(+item.attr('ry').value); - let rx = +(item.attr('rx') || none).value; - let ry = +(item.attr('ry') || none).value; - - if (!hasRx && !hasRy) { - // If neither 'rx' nor 'ry' are properly specified, then set both rx and ry to 0. - rx = ry = 0; - } else if (hasRx && !hasRy) { - // Otherwise, if a properly specified value is provided for 'rx', but not for 'ry', - // then set both rx and ry to the value of 'rx'. - ry = rx; - } else if (!hasRx && hasRy) { - // Otherwise, if a properly specified value is provided for 'ry', but not for 'rx', - // then set both rx and ry to the value of 'ry'. - rx = ry; - } else { - // If rx is greater than half of 'width', then set rx to half of 'width'. - if (rx > width / 2) { - rx = width / 2; - } - // If ry is greater than half of 'height', then set ry to half of 'height'. - if (ry > height / 2) { - ry = height / 2; - } - } - - // Values like '100%' compute to NaN, thus running after - // cleanupNumericValues when 'px' units has already been removed. - // TODO: Calculate sizes from % and non-px units if possible. - if (isNaN(x - y + width - height + rx - ry)) { - return undefined; - } - - let pathData; - if (!rx && !ry) { - pathData = `M ${x} ${y} H ${x + width} V ${y + height} H ${x} Z`; - } else { - pathData = - `M ${x + rx} ${y} ` + - `H ${x + width - rx} ` + - `A ${rx} ${ry} 0 0 1 ${x + width} ${y + ry} ` + - `V ${y + height - ry} ` + - `A ${rx} ${ry} 0 0 1 ${x + width - rx} ${y + height} ` + - `H ${x + rx} ` + - `A ${rx} ${ry} 0 0 1 ${x} ${y + height - ry} ` + - `V ${y + ry} ` + - `A ${rx} ${ry} 0 0 1 ${x + rx} ${y}`; - } - - item.addAttr({ name: 'd', value: pathData, prefix: '', local: 'd' }); - item.renameElem('path').removeAttr(['x', 'y', 'width', 'height', 'rx', 'ry']); -} - -function isValidCornerRadius(val: any) { - return !(typeof val !== 'number' || val === Infinity || val < 0); -} diff --git a/src/app/pages/editor/scripts/svgo/plugins/replaceUseElems.ts b/src/app/pages/editor/scripts/svgo/plugins/replaceUseElems.ts deleted file mode 100644 index db10aadc..00000000 --- a/src/app/pages/editor/scripts/svgo/plugins/replaceUseElems.ts +++ /dev/null @@ -1,158 +0,0 @@ -import * as CSSClassList from 'svgo/lib/svgo/css-class-list'; -import * as CSSStyleDeclaration from 'svgo/lib/svgo/css-style-declaration'; -import * as JSAPI from 'svgo/lib/svgo/jsAPI'; - -export const replaceUseElems = { - active: true, - type: 'full', - fn: replaceUseElemsFn, - params: undefined as any, -}; - -/** - * Replace elems with their referenced content. - * - * @param {Object} document the root document - * @param {Object} params plugin params - */ -function replaceUseElemsFn(document: any, params: any): any { - const defsElems = document.querySelectorAll('defs') || []; - - const queryReferencedElementFn = (selector: string) => { - for (const defs of defsElems) { - const referencedElem = defs.querySelector(selector); - if (referencedElem) { - return cloneParsedSvg(referencedElem); - } - } - return undefined; - }; - - // TODO: handle the case where a 'use' element references another 'use' - // TODO: handle the circular dependency that could potentially result as well - const useElems = document.querySelectorAll('use') || []; - for (const use of useElems) { - if (!use.hasAttr('xlink:href')) { - continue; - } - const refElem = queryReferencedElementFn(use.attr('xlink:href').value); - if (!refElem) { - continue; - } - use.removeAttr('xlink:href'); - - if (refElem.isElem('symbol')) { - // TODO: determine whether we should support 'symbol' elements as well - continue; - } - - const addAttrFn = function(elem: any, attrName: string, attrValue: string) { - elem.addAttr({ - name: attrName, - value: attrValue, - prefix: '', - local: attrName, - }); - }; - - if (refElem.isElem('svg')) { - // TODO: test this - const svg = refElem; - if (use.hasAttr('width')) { - addAttrFn(svg, 'width', use.attr('width').value); - use.removeAttr('width'); - } - if (use.hasAttr('height')) { - addAttrFn(svg, 'height', use.attr('height').value); - use.removeAttr('height'); - } - } - - // TODO: handle the NAN cases? - let x = 0; - let y = 0; - if (use.hasAttr('x')) { - x = +use.attr('x').value; - use.removeAttr('x'); - } - if (use.hasAttr('y')) { - y = +use.attr('y').value; - use.removeAttr('y'); - } - if (x || y) { - let transform = `translate(${x} ${y})`; - if (use.hasAttr('transform')) { - transform = use.attr('transform').value + ' ' + transform; - } - addAttrFn(use, 'transform', transform); - } - use.content = [refElem]; - refElem.parentNode = use; - use.renameElem('g'); - } - - return document; -} - -// Clone is currently broken. Hack it: -function cloneParsedSvg(svg: any): any { - const clones = new Map(); - - function cloneKeys(target: any, obj: any) { - for (const key of Object.keys(obj)) { - target[key] = clone(obj[key]); - } - return target; - } - - function clone(obj: any) { - if (typeof obj !== 'object' || obj === null) { - return obj; - } - - if (clones.has(obj)) { - return clones.get(obj); - } - - let objClone; - - if (obj.constructor === JSAPI) { - objClone = new JSAPI({}, obj.parentNode); - clones.set(obj, objClone); - - if (obj.parentNode) { - objClone.parentNode = clone(obj.parentNode); - } - cloneKeys(objClone, obj); - } else if ( - obj.constructor === CSSClassList || - obj.constructor === CSSStyleDeclaration || - obj.constructor === Object || - obj.constructor === Array - ) { - objClone = new obj.constructor(); - clones.set(obj, objClone); - cloneKeys(objClone, obj); - } else if (obj.constructor === Map) { - objClone = new Map(); - clones.set(obj, objClone); - - for (const [key, val] of obj) { - objClone.set(clone(key), clone(val)); - } - } else if (obj.constructor === Set) { - objClone = new Set(); - clones.set(obj, objClone); - - for (const val of obj) { - objClone.add(clone(val)); - } - } else { - throw Error('unexpected type'); - } - - return objClone; - } - - return clone(svg); -} diff --git a/src/app/pages/editor/services/StoreUtil.ts b/src/app/pages/editor/services/StoreUtil.ts deleted file mode 100644 index 108e26b1..00000000 --- a/src/app/pages/editor/services/StoreUtil.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ToolMode } from 'app/pages/editor/model/paper'; -import { - SetEditPathInfo, - SetRotateItemsInfo, - SetToolMode, - SetTransformPathsInfo, -} from 'app/pages/editor/store/paper/actions'; - -// TODO: expand on this class... possibly a better redesigned version? - -export function getEnterDefaultModeActions() { - return [ - new SetToolMode(ToolMode.Default), - new SetEditPathInfo(undefined), - new SetRotateItemsInfo(undefined), - new SetTransformPathsInfo(undefined), - ]; -} diff --git a/src/app/pages/editor/services/actionmode.service.ts b/src/app/pages/editor/services/actionmode.service.ts deleted file mode 100644 index 3975500a..00000000 --- a/src/app/pages/editor/services/actionmode.service.ts +++ /dev/null @@ -1,514 +0,0 @@ -import { Injectable } from '@angular/core'; -import { - ActionMode, - ActionSource, - Hover, - HoverType, - Selection, - SelectionType, -} from 'app/pages/editor/model/actionmode'; -import { MorphableLayer } from 'app/pages/editor/model/layers'; -import { Path, PathMutator, PathUtil } from 'app/pages/editor/model/paths'; -import { PathAnimationBlock } from 'app/pages/editor/model/timeline'; -import { AutoAwesome } from 'app/pages/editor/scripts/algorithms'; -import { Action, State, Store } from 'app/pages/editor/store'; -import { - SetActionMode, - SetActionModeHover, - SetActionModeSelections, - SetPairedSubPaths, - SetUnpairedSubPath, -} from 'app/pages/editor/store/actionmode/actions'; -import { - getActionMode, - getActionModeHover, - getActionModeSelections, - getPairedSubPaths, - getUnpairedSubPath, -} from 'app/pages/editor/store/actionmode/selectors'; -import { BatchAction } from 'app/pages/editor/store/batch/actions'; -import { SetAnimation } from 'app/pages/editor/store/timeline/actions'; -import * as _ from 'lodash'; -import { OutputSelector } from 'reselect'; -import { first } from 'rxjs/operators'; - -import { LayerTimelineService } from './layertimeline.service'; - -/** - * A simple service that provides an interface for making action mode changes. - */ -@Injectable({ providedIn: 'root' }) -export class ActionModeService { - constructor( - private readonly store: Store, - private readonly layerTimelineService: LayerTimelineService, - ) {} - - // Action mode. - - isActionMode() { - return this.getActionMode() !== ActionMode.None; - } - - getActionMode() { - return this.queryStore(getActionMode); - } - - setActionMode(mode: ActionMode) { - this.store.dispatch(new SetActionMode(mode)); - } - - toggleSplitCommandsMode() { - this.toggleActionMode(ActionMode.SplitCommands); - } - - toggleSplitSubPathsMode() { - this.toggleActionMode(ActionMode.SplitSubPaths); - } - - togglePairSubPathsMode() { - this.toggleActionMode(ActionMode.PairSubPaths); - } - - private toggleActionMode(modeToToggle: ActionMode) { - const currentMode = this.getActionMode(); - if (currentMode === ActionMode.None) { - return; - } - this.setActionMode(currentMode === modeToToggle ? ActionMode.Selection : modeToToggle); - } - - isShowingSubPathActionMode() { - return this.isShowingActionModeType(SelectionType.SubPath); - } - - isShowingSegmentActionMode() { - return this.isShowingActionModeType(SelectionType.Segment); - } - - isShowingPointActionMode() { - return this.isShowingActionModeType(SelectionType.Point); - } - - private isShowingActionModeType(type: SelectionType) { - const mode = this.getActionMode(); - return ( - mode !== ActionMode.None && - (mode !== ActionMode.Selection || this.getSelections().filter(s => s.type === type).length) - ); - } - - closeActionMode() { - const mode = this.getActionMode(); - if (mode === ActionMode.None) { - return; - } - if (mode === ActionMode.Selection) { - if (this.queryStore(getActionModeSelections).length) { - // TODO: move this logic out into a component (it's confusing) - this.store.dispatch(new SetActionModeSelections([])); - } else { - this.store.dispatch(new SetActionMode(ActionMode.None)); - } - } else { - this.store.dispatch(new SetActionMode(ActionMode.Selection)); - } - } - - // Selections. - - setSelections(selections: ReadonlyArray) { - this.store.dispatch(new SetActionModeSelections(selections)); - } - - getSelections() { - return this.queryStore(getActionModeSelections); - } - - toggleSubPathSelection(source: ActionSource, subIdx: number) { - const selections = [...this.getSelections()]; - _.remove(selections, s => s.type !== SelectionType.SubPath || s.source !== source); - const type = SelectionType.SubPath; - const toggledSelections = this.toggleSelections(selections, [{ type, source, subIdx }]); - this.store.dispatch(new SetActionModeSelections(toggledSelections)); - } - - toggleSegmentSelections( - source: ActionSource, - segments: ReadonlyArray<{ subIdx: number; cmdIdx: number }>, - ) { - const selections = [...this.getSelections()]; - _.remove(selections, s => s.type !== SelectionType.Segment || s.source !== source); - const type = SelectionType.Segment; - const toggledSelections = this.toggleSelections( - selections, - segments.map(({ subIdx, cmdIdx }) => ({ type, source, subIdx, cmdIdx })), - ); - this.store.dispatch(new SetActionModeSelections(toggledSelections)); - } - - togglePointSelection( - source: ActionSource, - subIdx: number, - cmdIdx: number, - isShiftOrMetaPressed: boolean, - ) { - const selections = [...this.getSelections()]; - _.remove(selections, s => s.type !== SelectionType.Point || s.source !== source); - const type = SelectionType.Point; - const toggledSelections = this.toggleSelections( - selections, - [{ type, source, subIdx, cmdIdx }], - isShiftOrMetaPressed, - ); - this.store.dispatch(new SetActionModeSelections(toggledSelections)); - } - - /** - * Toggles the specified shape shifter selections. If a selection exists, all selections - * will be removed from the list. Otherwise, they will be added to the list of selections. - * By default, all other selections from the list will be cleared. - */ - private toggleSelections( - currentSelections: Selection[], - newSelections: Selection[], - appendToList = false, - ) { - const matchingSelections = _.remove(currentSelections, currSel => { - // Remove any selections that are equal to a new selection. - return newSelections.some(s => _.isEqual(s, currSel)); - }); - if (!matchingSelections.length) { - // If no selections were removed, then add all of the selections to the list. - currentSelections.push(...newSelections); - } - if (!appendToList) { - // If we aren't appending multiple selections at a time, then clear - // any previous selections from the list. - _.remove(currentSelections, currSel => { - return newSelections.every(newSel => !_.isEqual(currSel, newSel)); - }); - } - return currentSelections; - } - - // Hovers. - - setHover(newHover: Hover) { - const currHover = this.queryStore(getActionModeHover); - if (!_.isEqual(newHover, currHover)) { - this.store.dispatch(new SetActionModeHover(newHover)); - } - } - - splitInHalfHover() { - const pointSelections = this.getSelections().filter(s => s.type === SelectionType.Point); - if (pointSelections.length) { - const { source, subIdx, cmdIdx } = pointSelections[0]; - this.setHover({ type: HoverType.Split, source, subIdx, cmdIdx }); - } - } - - clearHover() { - this.setHover(undefined); - } - - // Mutate subpaths. - - reverseSelectedSubPaths() { - this.mutateSelectedSubPaths((pm, subIdx) => pm.reverseSubPath(subIdx)); - } - - shiftBackSelectedSubPaths() { - this.mutateSelectedSubPaths((pm, subIdx) => pm.shiftSubPathBack(subIdx)); - } - - shiftForwardSelectedSubPaths() { - this.mutateSelectedSubPaths((pm, subIdx) => pm.shiftSubPathForward(subIdx)); - } - - private mutateSelectedSubPaths(mutatorFn: (pm: PathMutator, subIdx: number) => void) { - const selections = this.getSelections().filter(s => s.type === SelectionType.SubPath); - const { source } = selections[0]; - const pm = this.getActivePathBlockValue(source).mutate(); - for (const { subIdx } of selections) { - mutatorFn(pm, subIdx); - } - this.store.dispatch( - new BatchAction( - this.buildUpdatedActivePathBlockAnimationAction(source, pm.build()), - new SetActionModeHover(undefined), - ), - ); - } - - // Mutate points. - - shiftPointToFront() { - const selections = this.getSelections().filter(s => s.type === SelectionType.Point); - const { source, subIdx, cmdIdx } = selections[0]; - const activePath = this.getActivePathBlockValue(source); - const pm = activePath.mutate(); - pm.shiftSubPathForward(subIdx, cmdIdx); - this.store.dispatch(this.buildUpdatedActivePathBlockAnimationAction(source, pm.build())); - } - - splitSelectedPointInHalf() { - const selections = this.getSelections().filter(s => s.type === SelectionType.Point); - const { source, subIdx, cmdIdx } = selections[0]; - const activePath = this.getActivePathBlockValue(source); - const pm = activePath.mutate(); - pm.splitCommandInHalf(subIdx, cmdIdx); - this.store.dispatch( - new BatchAction( - this.buildUpdatedActivePathBlockAnimationAction(source, pm.build()), - new SetActionModeSelections([]), - new SetActionModeHover(undefined), - ), - ); - } - - // Pair/unpair subpaths. - - pairSubPath(subIdx: number, actionSource: ActionSource) { - const currUnpair = this.getUnpairedSubPath(); - const actions: Action[] = []; - if (currUnpair && actionSource !== currUnpair.source) { - const { source: fromSource, subIdx: fromSubIdx } = currUnpair; - const toSource = actionSource; - const toSubIdx = subIdx; - actions.push(new SetUnpairedSubPath(undefined)); - const fromSelections = this.getSelections().filter(s => s.source === fromSource); - const toSelections = this.getSelections().filter(s => s.source === toSource); - if (fromSelections.length) { - actions.push( - new SetActionModeSelections( - fromSelections.map(s => { - const { subIdx: sIdx, cmdIdx, source, type } = s; - return { - subIdx: sIdx === fromSubIdx ? 0 : sIdx, - cmdIdx, - source, - type, - }; - }), - ), - ); - } else if (toSelections.length) { - actions.push( - new SetActionModeSelections( - toSelections.map(s => { - const { subIdx: sIdx, cmdIdx, source, type } = s; - return { - subIdx: sIdx === toSubIdx ? 0 : sIdx, - cmdIdx, - source, - type, - }; - }), - ), - ); - } - const pairedSubPaths = new Set(); - this.getPairedSubPaths().forEach(p => pairedSubPaths.add(p)); - if (pairedSubPaths.has(fromSubIdx)) { - pairedSubPaths.delete(fromSubIdx); - } - if (pairedSubPaths.has(toSubIdx)) { - pairedSubPaths.delete(toSubIdx); - } - pairedSubPaths.add(pairedSubPaths.size); - actions.push(new SetPairedSubPaths(pairedSubPaths)); - actions.push(new SetActionModeHover(undefined)); - let updatedAnimation = this.buildUpdatedActivePathBlockAnimation( - fromSource, - this.getActivePathBlockValue(fromSource) - .mutate() - .moveSubPath(fromSubIdx, 0) - .build(), - ); - updatedAnimation = this.buildUpdatedActivePathBlockAnimation( - toSource, - this.getActivePathBlockValue(toSource) - .mutate() - .moveSubPath(toSubIdx, 0) - .build(), - updatedAnimation, - ); - actions.push(new SetAnimation(updatedAnimation)); - } else { - actions.push(new SetUnpairedSubPath({ source: actionSource, subIdx })); - } - this.store.dispatch(new BatchAction(...actions)); - } - - private getPairedSubPaths() { - return this.queryStore(getPairedSubPaths); - } - - setUnpairedSubPath(unpair: { subIdx: number; source: ActionSource }) { - if (!_.isEqual(this.getUnpairedSubPath(), unpair)) { - this.store.dispatch(new SetUnpairedSubPath(unpair)); - } - } - - private getUnpairedSubPath() { - return this.queryStore(getUnpairedSubPath); - } - - // Autofix. - - autoFix() { - const [from, to] = AutoAwesome.autoFix( - this.getActivePathBlockValue(ActionSource.From), - this.getActivePathBlockValue(ActionSource.To), - ); - let animation = this.buildUpdatedActivePathBlockAnimation(ActionSource.From, from); - animation = this.buildUpdatedActivePathBlockAnimation(ActionSource.To, to, animation); - this.store.dispatch(new SetAnimation(animation)); - } - - // Delete selected action mode models. - - deleteSelectedActionModeModels() { - if (this.getActionMode() !== ActionMode.Selection) { - return; - } - const selections = this.getSelections(); - if (!selections.length) { - return; - } - const subPathSelections = selections.filter(s => s.type === SelectionType.SubPath); - const segmentSelections = selections.filter(s => s.type === SelectionType.Segment); - const pointSelections = selections.filter(s => s.type === SelectionType.Point); - let updatePathAction: SetAnimation; - if (subPathSelections.length) { - const { source, subIdx } = subPathSelections[0]; - const path = this.getActivePathBlockValue(source); - if (path.getSubPath(subIdx).isSplit()) { - const pm = path.mutate(); - const layer = this.getActivePathBlockLayer(); - if (layer.isFilled()) { - pm.deleteFilledSubPath(subIdx); - } else if (layer.isStroked()) { - pm.deleteStrokedSubPath(subIdx); - } - updatePathAction = this.buildUpdatedActivePathBlockAnimationAction(source, pm.build()); - } - } else if (segmentSelections.length) { - const { source, subIdx, cmdIdx } = segmentSelections[0]; - const path = this.getActivePathBlockValue(source); - if (path.getCommand(subIdx, cmdIdx).isSplitSegment()) { - updatePathAction = this.buildUpdatedActivePathBlockAnimationAction( - source, - path - .mutate() - .deleteFilledSubPathSegment(subIdx, cmdIdx) - .build(), - ); - } - } else if (pointSelections.length) { - const source = pointSelections[0].source; - const path = this.getActivePathBlockValue(source); - const unsplitOpsMap = new Map>(); - for (const { subIdx, cmdIdx } of pointSelections) { - if (!path.getCommand(subIdx, cmdIdx).isSplitPoint()) { - continue; - } - let subIdxOps = unsplitOpsMap.get(subIdx); - if (!subIdxOps) { - subIdxOps = []; - } - subIdxOps.push({ subIdx, cmdIdx }); - unsplitOpsMap.set(subIdx, subIdxOps); - } - if (unsplitOpsMap.size) { - const pm = path.mutate(); - unsplitOpsMap.forEach((ops, idx) => { - PathUtil.sortPathOps(ops); - ops.forEach(op => pm.unsplitCommand(op.subIdx, op.cmdIdx)); - }); - updatePathAction = this.buildUpdatedActivePathBlockAnimationAction(source, pm.build()); - } - } - if (updatePathAction) { - this.store.dispatch( - new BatchAction( - updatePathAction, - new SetActionModeSelections([]), - new SetActionModeHover(undefined), - ), - ); - } - } - - // Update active path block. - - updateActivePathBlock(source: ActionSource, path: Path) { - this.store.dispatch(this.buildUpdatedActivePathBlockAnimationAction(source, path)); - } - - private buildUpdatedActivePathBlockAnimationAction( - source: ActionSource, - path: Path, - animation = this.layerTimelineService.getAnimation(), - ) { - return new SetAnimation(this.buildUpdatedActivePathBlockAnimation(source, path, animation)); - } - - private buildUpdatedActivePathBlockAnimation( - source: ActionSource, - path: Path, - animation = this.layerTimelineService.getAnimation(), - ) { - const blockId = this.getActivePathBlock().id; - const blockIndex = _.findIndex(animation.blocks, b => b.id === blockId); - const block = animation.blocks[blockIndex] as PathAnimationBlock; - - // Remove any existing conversions and collapsing sub paths from the path. - const oppSource = source === ActionSource.From ? ActionSource.To : ActionSource.From; - let oppPath = oppSource === ActionSource.From ? block.fromValue : block.toValue; - [path, oppPath] = AutoAwesome.autoAddCollapsingSubPaths(path, oppPath); - [path, oppPath] = AutoAwesome.autoConvert(path, oppPath); - - const setBlockValueFn = (b: PathAnimationBlock, t: ActionSource, p: Path) => { - if (t === ActionSource.From) { - b.fromValue = p; - } else { - b.toValue = p; - } - }; - - const newBlock = block.clone(); - setBlockValueFn(newBlock, source, path); - setBlockValueFn(newBlock, oppSource, oppPath); - - const newBlocks = animation.blocks.map((b, i) => (i === blockIndex ? newBlock : b)); - animation = animation.clone(); - animation.blocks = newBlocks; - return animation; - } - - private getActivePathBlock() { - return this.layerTimelineService.getSelectedBlocks()[0] as PathAnimationBlock; - } - - private getActivePathBlockValue(source: ActionSource) { - const activeBlock = this.getActivePathBlock(); - return source === ActionSource.From ? activeBlock.fromValue : activeBlock.toValue; - } - - private getActivePathBlockLayer() { - const vl = this.layerTimelineService.getVectorLayer(); - return vl.findLayerById(this.getActivePathBlock().layerId) as MorphableLayer; - } - - private queryStore(selector: OutputSelector T>) { - let obj: T; - this.store - .select(selector) - .pipe(first()) - .subscribe(o => (obj = o)); - return obj; - } -} diff --git a/src/app/pages/editor/services/clipboard.service.ts b/src/app/pages/editor/services/clipboard.service.ts deleted file mode 100644 index 7eb01908..00000000 --- a/src/app/pages/editor/services/clipboard.service.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Injectable } from '@angular/core'; -import { AnimationBlock } from 'app/pages/editor/model/timeline'; -import { bugsnagClient } from 'app/pages/editor/scripts/bugsnag'; -import { SvgLoader, VectorDrawableLoader } from 'app/pages/editor/scripts/import'; -import * as $ from 'jquery'; - -import { ActionModeService } from './actionmode.service'; -import { LayerTimelineService } from './layertimeline.service'; -import { PlaybackService } from './playback.service'; - -declare const ga: Function; - -@Injectable({ providedIn: 'root' }) -export class ClipboardService { - constructor( - private readonly layerTimelineService: LayerTimelineService, - private readonly playbackService: PlaybackService, - private readonly actionModeService: ActionModeService, - ) {} - - init() { - const cutCopyHandlerFn = (event: JQuery.Event, shouldCut: boolean) => { - if (document.activeElement.matches('input')) { - return true; - } - - const blocks = this.layerTimelineService.getSelectedBlocks().map(b => b.toJSON()); - if (!blocks.length) { - return false; - } - const clipboardData = (event.originalEvent as ClipboardEvent).clipboardData; - clipboardData.setData('text/plain', JSON.stringify({ blocks }, undefined, 2)); - - if (shouldCut) { - this.layerTimelineService.deleteSelectedModels(); - } - - return false; - }; - - const pasteHandlerFn = (event: JQuery.Event) => { - if (this.actionModeService.isActionMode()) { - // TODO: make action mode automatically exit when layers/blocks are added in other parts of the app - bugsnagClient.notify('Attempt to import files while in action mode', { - severity: 'warning', - }); - return false; - } - if (document.activeElement.matches('input')) { - return true; - } - - const clipboardData = (event.originalEvent as ClipboardEvent).clipboardData; - const str = clipboardData.getData('text'); - const existingVl = this.layerTimelineService.getVectorLayer(); - - if (str.match(/<\/svg>\s*$/)) { - // Paste SVG. - ga('send', 'event', 'paste', 'svg'); - SvgLoader.loadVectorLayerFromSvgString(str, name => !!existingVl.findLayerByName(name)) - .then(vl => this.layerTimelineService.importLayers([vl])) - .catch(() => console.warn('failed to import SVG')); - } else if (str.match(/<\/vector>\s*$/)) { - // Paste VD. - ga('send', 'event', 'paste', 'vd'); - const importedVl = VectorDrawableLoader.loadVectorLayerFromXmlString( - str, - name => !!existingVl.findLayerByName(name), - ); - if (importedVl) { - this.layerTimelineService.importLayers([importedVl]); - } - } else if (str.match(/\}\s*$/)) { - let parsed; - try { - parsed = JSON.parse(str); - } catch (e) { - console.error(`Couldn't parse JSON: ${str}`); - return false; - } - if (parsed.blocks) { - ga('send', 'event', 'paste', 'json.blocks'); - this.layerTimelineService.addBlocks( - parsed.blocks.map((b: any) => { - const block = AnimationBlock.from(b); - const { - layerId, - propertyName, - fromValue, - toValue, - interpolator, - startTime, - endTime, - } = block; - const duration = endTime - startTime; - return { - layerId, - propertyName, - fromValue, - toValue, - currentTime: this.playbackService.getCurrentTime(), - duration, - interpolator, - }; - }), - false, - ); - } else { - ga('send', 'event', 'paste', 'json.unknown'); - } - return false; - } - - return false; - }; - - const cutHandler = (event: JQuery.Event) => cutCopyHandlerFn(event, true); - const copyHandler = (event: JQuery.Event) => cutCopyHandlerFn(event, false); - const pasteHandler = pasteHandlerFn; - - $(window) - .on('cut', cutHandler) - .on('copy', copyHandler) - .on('paste', pasteHandler); - } - - destroy() { - $(window) - .unbind('cut') - .unbind('copy') - .unbind('paste'); - } -} diff --git a/src/app/pages/editor/services/fileexport.service.ts b/src/app/pages/editor/services/fileexport.service.ts deleted file mode 100644 index c971cf58..00000000 --- a/src/app/pages/editor/services/fileexport.service.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { Injectable } from '@angular/core'; -import { LayerUtil, VectorLayer } from 'app/pages/editor/model/layers'; -import { Animation } from 'app/pages/editor/model/timeline'; -import { AvdSerializer, SpriteSerializer, SvgSerializer } from 'app/pages/editor/scripts/export'; -import { State, Store } from 'app/pages/editor/store'; -import { getHiddenLayerIds, getVectorLayer } from 'app/pages/editor/store/layers/selectors'; -import { getAnimation } from 'app/pages/editor/store/timeline/selectors'; -import * as $ from 'jquery'; -import * as JSZip from 'jszip'; -import * as _ from 'lodash'; -import { first } from 'rxjs/operators'; - -// Store a version number just in case we ever change the export format... -const IMPORT_EXPORT_VERSION = 1; - -const EXPORTED_FPS = [30, 60]; - -/** - * A simple service that exports vectors and animations. - */ -@Injectable({ providedIn: 'root' }) -export class FileExportService { - static fromJSON(jsonObj: any) { - const { layers, timeline } = jsonObj; - const vectorLayer = new VectorLayer(layers.vectorLayer); - const hiddenLayerIds = new Set(layers.hiddenLayerIds); - const animation = new Animation(timeline.animation); - return { vectorLayer, hiddenLayerIds, animation }; - } - - constructor(private readonly store: Store) {} - - exportJSON() { - const vl = this.getVectorLayer(); - const anim = this.getAnimation(); - const jsonStr = JSON.stringify( - { - version: IMPORT_EXPORT_VERSION, - layers: { - vectorLayer: vl.toJSON(), - hiddenLayerIds: Array.from(this.getHiddenLayerIds()), - }, - timeline: { - animation: anim.toJSON(), - }, - }, - undefined, - 2, - ); - downloadFile(jsonStr, `${vl.name}.shapeshifter`); - } - - exportSvg() { - // Export standalone SVG frames. - const vl = this.getVectorLayerWithoutHiddenLayers(); - const anim = this.getAnimationWithoutHiddenBlocks(); - if (!anim.blocks.length) { - // Just export an SVG if there are no animation blocks defined. - const svg = SvgSerializer.toSvgString(vl); - downloadFile(svg, `${vl.name}.svg`); - return; - } - // TODO: figure out how to add better jszip typings - const zip = new JSZip(); - EXPORTED_FPS.forEach(fps => { - const numSteps = Math.ceil((anim.duration / 1000) * fps); - const svgs = SpriteSerializer.createSvgFrames(vl, anim, numSteps); - const length = (numSteps - 1).toString().length; - const fpsFolder = zip.folder(`${fps}fps`); - svgs.forEach((s, i) => { - fpsFolder.file(`frame${_.padStart(i.toString(), length, '0')}.svg`, s); - }); - }); - zip.generateAsync({ type: 'blob' }).then((content: Blob) => { - downloadFile(content, `frames_${vl.name}.zip`); - }); - } - - // TODO: should we or should we not export hidden layers? - exportVectorDrawable() { - const vl = this.getVectorLayerWithoutHiddenLayers(); - const vd = AvdSerializer.toVectorDrawableXmlString(vl); - const fileName = `vd_${vl.name}.xml`; - downloadFile(vd, fileName); - } - - exportAnimatedVectorDrawable() { - const vl = this.getVectorLayerWithoutHiddenLayers(); - const anim = this.getAnimationWithoutHiddenBlocks(); - const avd = AvdSerializer.toAnimatedVectorDrawableXmlString(vl, anim); - const fileName = `avd_${anim.name}.xml`; - downloadFile(avd, fileName); - } - - exportSvgSpritesheet() { - // Create an svg sprite animation. - const vl = this.getVectorLayerWithoutHiddenLayers(); - const anim = this.getAnimationWithoutHiddenBlocks(); - // TODO: figure out how to add better jszip typings - const zip = new JSZip(); - (async () => { - await asyncForEach(EXPORTED_FPS, async fps => { - const numSteps = Math.ceil((anim.duration / 1000) * fps); - const svgSprite = await SpriteSerializer.createSvgSprite(vl, anim, numSteps); - const cssSprite = SpriteSerializer.createCss(vl.width, vl.height, anim.duration, numSteps); - const fileName = `sprite_${fps}fps`; - const htmlSprite = SpriteSerializer.createHtml(`${fileName}.svg`, `${fileName}.css`); - const spriteFolder = zip.folder(`${fps}fps`); - spriteFolder.file(`${fileName}.html`, htmlSprite); - spriteFolder.file(`${fileName}.css`, cssSprite); - spriteFolder.file(`${fileName}.svg`, svgSprite); - }); - zip.generateAsync({ type: 'blob' }).then((content: Blob) => { - downloadFile(content, `spritesheet_${vl.name}.zip`); - }); - })(); - } - - exportCssKeyframes() { - // TODO: implement this - } - - private getVectorLayer() { - let vectorLayer: VectorLayer; - this.store - .select(getVectorLayer) - .pipe(first()) - .subscribe(vl => (vectorLayer = vl)); - return vectorLayer; - } - - private getAnimation() { - let animation: Animation; - this.store - .select(getAnimation) - .pipe(first()) - .subscribe(anim => (animation = anim)); - return animation; - } - - private getHiddenLayerIds() { - let hiddenLayerIds: ReadonlySet; - this.store - .select(getHiddenLayerIds) - .pipe(first()) - .subscribe(ids => (hiddenLayerIds = ids)); - return hiddenLayerIds; - } - - private getVectorLayerWithoutHiddenLayers() { - return LayerUtil.removeLayers(this.getVectorLayer(), ...Array.from(this.getHiddenLayerIds())); - } - - private getAnimationWithoutHiddenBlocks() { - const anim = this.getAnimation().clone(); - const hiddenLayerIds = this.getHiddenLayerIds(); - anim.blocks = anim.blocks.filter(b => !hiddenLayerIds.has(b.layerId)); - return anim; - } -} - -function downloadFile(content: string | Blob, fileName: string) { - const anchor = $('') - .hide() - .appendTo(document.body); - const blob = content instanceof Blob ? content : new Blob([content], { type: 'octet/stream' }); - const url = window.URL.createObjectURL(blob); - anchor.attr({ href: url, download: fileName }); - anchor.get(0).click(); - window.URL.revokeObjectURL(url); -} - -async function asyncForEach( - array: number[], - callback: (value: number, index: number, array: number[]) => void, -) { - for (let index = 0; index < array.length; index++) { - await callback(array[index], index, array); - } -} diff --git a/src/app/pages/editor/services/fileimport.service.ts b/src/app/pages/editor/services/fileimport.service.ts deleted file mode 100644 index 0804b093..00000000 --- a/src/app/pages/editor/services/fileimport.service.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { Injectable } from '@angular/core'; -import { LayerUtil, VectorLayer } from 'app/pages/editor/model/layers'; -import { Animation } from 'app/pages/editor/model/timeline'; -import { ModelUtil } from 'app/pages/editor/scripts/common'; -import { SvgLoader, VectorDrawableLoader } from 'app/pages/editor/scripts/import'; -import { State, Store } from 'app/pages/editor/store'; -import { getVectorLayer } from 'app/pages/editor/store/layers/selectors'; -import { ResetWorkspace } from 'app/pages/editor/store/reset/actions'; -import { first } from 'rxjs/operators'; - -import { FileExportService } from './fileexport.service'; -import { LayerTimelineService } from './layertimeline.service'; -import { Duration, SnackBarService } from './snackbar.service'; - -declare const ga: Function; - -enum ImportType { - Svg = 1, - VectorDrawable, - Json, -} - -/** - * A simple service that imports vector layers from files. - */ -@Injectable({ providedIn: 'root' }) -export class FileImportService { - constructor( - private readonly store: Store, - private readonly snackBarService: SnackBarService, - private readonly layerTimelineService: LayerTimelineService, - ) {} - - private get vectorLayer() { - let vectorLayer: VectorLayer; - this.store - .select(getVectorLayer) - .pipe(first()) - .subscribe(vl => (vectorLayer = vl)); - return vectorLayer; - } - - import(fileList: FileList, resetWorkspace = false) { - if (!fileList || !fileList.length) { - return; - } - - const files: File[] = []; - // tslint:disable-next-line: prefer-for-of - for (let i = 0; i < fileList.length; i++) { - files.push(fileList[i]); - } - - let numCallbacks = 0; - let numErrors = 0; - const addedVls: VectorLayer[] = []; - - let importType: ImportType; - const maybeAddVectorLayersFn = () => { - numCallbacks++; - if (numErrors === files.length) { - this.onFailure(); - } else if (numCallbacks === files.length) { - this.onSuccess(importType, resetWorkspace, addedVls); - } - }; - - const existingVl = this.vectorLayer; - for (const file of files) { - const fileReader = new FileReader(); - - fileReader.onload = event => { - const text = (event.target as any).result; - const callbackFn = (vectorLayer: VectorLayer) => { - if (!vectorLayer) { - numErrors++; - maybeAddVectorLayersFn(); - return; - } - addedVls.push(vectorLayer); - maybeAddVectorLayersFn(); - }; - const doesNameExistFn = (name: string) => { - return !!LayerUtil.findLayerByName([existingVl, ...addedVls], name); - }; - if (file.type.includes('svg')) { - importType = ImportType.Svg; - SvgLoader.loadVectorLayerFromSvgString(text, doesNameExistFn) - .then(vl => callbackFn(vl)) - .catch(() => { - console.warn('failed to import SVG'); - callbackFn(undefined); - }); - } else if (file.type.includes('xml')) { - importType = ImportType.VectorDrawable; - let vl: VectorLayer; - try { - vl = VectorDrawableLoader.loadVectorLayerFromXmlString(text, doesNameExistFn); - callbackFn(vl); - } catch (e) { - console.warn('Failed to parse the file', e); - callbackFn(undefined); - } - } else if (file.type === 'application/json' || file.name.match(/\.shapeshifter$/)) { - importType = ImportType.Json; - let vl: VectorLayer; - let animation: Animation; - let hiddenLayerIds: ReadonlySet; - try { - const jsonObj = JSON.parse(text); - const parsedObj = FileExportService.fromJSON(jsonObj); - vl = parsedObj.vectorLayer; - animation = parsedObj.animation; - hiddenLayerIds = parsedObj.hiddenLayerIds; - const regeneratedModels = ModelUtil.regenerateModelIds(vl, animation, hiddenLayerIds); - vl = regeneratedModels.vectorLayer; - animation = regeneratedModels.animation; - hiddenLayerIds = regeneratedModels.hiddenLayerIds; - } catch (e) { - console.warn('Failed to parse the file', e); - this.onFailure(); - } - this.onSuccess(importType, resetWorkspace, [vl], animation, hiddenLayerIds); - } - }; - - fileReader.onerror = event => { - const target = event.target as any; - switch (target.error.code) { - case target.error.NOT_FOUND_ERR: - alert('File not found'); - break; - case target.error.NOT_READABLE_ERR: - alert('File is not readable'); - break; - case target.error.ABORT_ERR: - break; - default: - alert('An error occurred reading this file'); - break; - } - numErrors++; - maybeAddVectorLayersFn(); - }; - - fileReader.onabort = event => { - alert('File read cancelled'); - }; - - fileReader.readAsText(file); - } - } - - private onSuccess( - importType: ImportType, - resetWorkspace: boolean, - vls: ReadonlyArray, - animation?: Animation, - hiddenLayerIds?: ReadonlySet, - ) { - if (importType === ImportType.Json) { - ga('send', 'event', 'Import', 'JSON'); - this.store.dispatch(new ResetWorkspace(vls[0], animation, hiddenLayerIds)); - } else { - if (importType === ImportType.Svg) { - ga('send', 'event', 'Import', 'SVG'); - } else if (importType === ImportType.VectorDrawable) { - ga('send', 'event', 'Import', 'Vector Drawable'); - } - if (resetWorkspace) { - this.store.dispatch(new ResetWorkspace()); - } - this.layerTimelineService.importLayers(vls); - // TODO: count number of individual layers? - this.snackBarService.show( - `Imported ${vls.length} layer${vls.length === 1 ? '' : 's'}`, - 'Dismiss', - Duration.Short, - ); - } - } - - private onFailure() { - this.snackBarService.show(`Couldn't import layers from file`, 'Dismiss', Duration.Long); - } -} diff --git a/src/app/pages/editor/services/index.ts b/src/app/pages/editor/services/index.ts deleted file mode 100644 index 865972af..00000000 --- a/src/app/pages/editor/services/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { ActionModeService } from './actionmode.service'; -export { ClipboardService } from './clipboard.service'; -export { FileExportService } from './fileexport.service'; -export { FileImportService } from './fileimport.service'; -export { LayerTimelineService } from './layertimeline.service'; -export { PlaybackService } from './playback.service'; -export { SnackBarService } from './snackbar.service'; -export { ShortcutService } from './shortcut.service'; -export { ThemeService } from './theme.service'; -export { PaperService } from './paper.service'; diff --git a/src/app/pages/editor/services/layertimeline.service.ts b/src/app/pages/editor/services/layertimeline.service.ts deleted file mode 100644 index 83966dba..00000000 --- a/src/app/pages/editor/services/layertimeline.service.ts +++ /dev/null @@ -1,717 +0,0 @@ -import { Injectable } from '@angular/core'; -import { INTERPOLATORS } from 'app/pages/editor/model/interpolators'; -import { - ClipPathLayer, - GroupLayer, - Layer, - LayerUtil, - PathLayer, - VectorLayer, -} from 'app/pages/editor/model/layers'; -import { Animation, AnimationBlock, PathAnimationBlock } from 'app/pages/editor/model/timeline'; -import { MathUtil, Matrix, ModelUtil } from 'app/pages/editor/scripts/common'; -import { Action, State, Store } from 'app/pages/editor/store'; -import { BatchAction } from 'app/pages/editor/store/batch/actions'; -import { - SetCollapsedLayers, - SetHiddenLayers, - SetSelectedLayers, - SetVectorLayer, -} from 'app/pages/editor/store/layers/actions'; -import { - getCollapsedLayerIds, - getHiddenLayerIds, - getSelectedLayerIds, - getVectorLayer, -} from 'app/pages/editor/store/layers/selectors'; -import { - SelectAnimation, - SetAnimation, - SetSelectedBlocks, -} from 'app/pages/editor/store/timeline/actions'; -import { - getAnimation, - getSelectedBlockIds, - isAnimationSelected, -} from 'app/pages/editor/store/timeline/selectors'; -import { environment } from 'environments/environment'; -import * as _ from 'lodash'; -import { OutputSelector } from 'reselect'; -import { first } from 'rxjs/operators'; - -import * as StoreUtil from './StoreUtil'; - -/** - * A simple service that provides an interface for making layer/timeline changes. - */ -@Injectable({ providedIn: 'root' }) -export class LayerTimelineService { - constructor(private readonly store: Store) {} - - /** - * Selects or deselects the animation. - */ - selectAnimation(isSelected: boolean) { - this.updateSelections(isSelected, new Set(), new Set()); - } - - /** - * Selects or deselects the specified block ID. - */ - selectBlock(blockId: string, clearExisting: boolean) { - const selectedBlockIds = this.getSelectedBlockIds(); - if (clearExisting) { - selectedBlockIds.forEach(id => { - if (id !== blockId) { - selectedBlockIds.delete(id); - } - }); - } - if (!clearExisting && selectedBlockIds.has(blockId)) { - selectedBlockIds.delete(blockId); - } else { - selectedBlockIds.add(blockId); - } - this.updateSelections(false, selectedBlockIds, new Set()); - } - - /** - * Selects or deselects the specified layer ID. - */ - selectLayer(layerId: string, clearExisting: boolean) { - const selectedLayerIds = this.getSelectedLayerIds(); - if (clearExisting) { - selectedLayerIds.forEach(id => { - if (id !== layerId) { - selectedLayerIds.delete(id); - } - }); - } - if (!clearExisting && selectedLayerIds.has(layerId)) { - selectedLayerIds.delete(layerId); - } else { - selectedLayerIds.add(layerId); - } - this.updateSelections(false, new Set(), selectedLayerIds); - } - - setSelectedLayers(layerIds: Set) { - this.updateSelections(false, new Set(), new Set(layerIds)); - } - - /** - * Clears all animation/block/layer selections. - */ - clearSelections() { - const actions = this.getClearSelectionsActions(); - if (actions.length) { - this.store.dispatch(new BatchAction(...actions)); - } - } - - private getClearSelectionsActions() { - return this.getUpdateSelectionsActions(false, new Set(), new Set()); - } - - private updateSelections( - isAnimSelected: boolean, - selectedBlockIds: ReadonlySet, - selectedLayerIds: ReadonlySet, - ) { - const actions = this.getUpdateSelectionsActions( - isAnimSelected, - selectedBlockIds, - selectedLayerIds, - ); - if (actions.length) { - this.store.dispatch(new BatchAction(...actions)); - } - } - - private getUpdateSelectionsActions( - isAnimSelected: boolean, - selectedBlockIds: ReadonlySet, - selectedLayerIds: ReadonlySet, - ) { - const actions: Action[] = []; - if (this.isAnimationSelected() !== isAnimSelected) { - actions.push(new SelectAnimation(isAnimSelected)); - } - if (!_.isEqual(this.getSelectedBlockIds(), selectedBlockIds)) { - actions.push(new SetSelectedBlocks(selectedBlockIds)); - } - if (!_.isEqual(this.getSelectedLayerIds(), selectedLayerIds)) { - actions.push(new SetSelectedLayers(selectedLayerIds)); - // TODO: improve this design somehow (probably best not to have this service depend on paper ops?) - // TODO: figure out which selection-changed cases should force you into default mode - // TODO: i.e. should selecting a new layer in edit path mode trigger edit path mode for the new layer? - if (environment.beta) { - actions.push(...StoreUtil.getEnterDefaultModeActions()); - } - } - return actions; - } - - /** - * Toggles the specified layer's expanded state. - */ - toggleExpandedLayer(layerId: string, recursive: boolean) { - const layerIds = new Set([layerId]); - if (recursive) { - const layer = this.getVectorLayer().findLayerById(layerId); - if (layer) { - layer.walk(l => layerIds.add(l.id)); - } - } - const collapsedLayerIds = this.getCollapsedLayerIds(); - if (collapsedLayerIds.has(layerId)) { - layerIds.forEach(id => collapsedLayerIds.delete(id)); - } else { - layerIds.forEach(id => collapsedLayerIds.add(id)); - } - this.store.dispatch(new SetCollapsedLayers(collapsedLayerIds)); - } - - /** - * Toggles the specified layer's visibility. - */ - toggleVisibleLayer(layerId: string) { - const layerIds = this.getHiddenLayerIds(); - if (layerIds.has(layerId)) { - layerIds.delete(layerId); - } else { - layerIds.add(layerId); - } - this.store.dispatch(new SetHiddenLayers(layerIds)); - } - - /** - * Imports a list of vector layers into the workspace. - */ - importLayers(vls: ReadonlyArray) { - if (!vls.length) { - return; - } - const importedVls = [...vls]; - const vectorLayer = this.getVectorLayer(); - let vectorLayers = [vectorLayer]; - if (!vectorLayer.children.length) { - // Simply replace the empty vector layer rather than merging with it. - const vl = importedVls[0].clone(); - vl.name = vectorLayer.name; - importedVls[0] = vl; - vectorLayers = []; - } - const newVectorLayers = [...vectorLayers, ...importedVls]; - const newVl = - newVectorLayers.length === 1 - ? newVectorLayers[0] - : newVectorLayers.reduce(LayerUtil.mergeVectorLayers); - this.store.dispatch( - new BatchAction(...this.getClearSelectionsActions(), new SetVectorLayer(newVl)), - ); - } - - /** - * Adds a layer to the vector tree. - */ - addLayer(layer: Layer) { - const vl = this.getVectorLayer(); - const selectedLayers = this.getSelectedLayers(); - if (selectedLayers.length === 1) { - const selectedLayer = selectedLayers[0]; - if (!(selectedLayer instanceof VectorLayer)) { - // Add the new layer as a sibling to the currently selected layer. - const parent = LayerUtil.findParent(vl, selectedLayer.id).clone(); - parent.children = [...parent.children, layer]; - this.updateLayer(parent); - return; - } - } - const vectorLayer = vl.clone(); - vectorLayer.children = [...vectorLayer.children, layer]; - this.updateLayer(vectorLayer); - } - - /** - * Sets the current vector layer. - */ - setVectorLayer(vl: VectorLayer) { - this.store.dispatch(new SetVectorLayer(vl)); - } - - /** - * Updates an existing layer in the tree. - */ - updateLayer(layer: Layer) { - this.store.dispatch(new SetVectorLayer(LayerUtil.updateLayer(this.getVectorLayer(), layer))); - } - - /** - * Replaces an existing layer in the tree with a new layer. Note that - * this method assumes that both layers still have the same children layers. - */ - swapLayers(layerId: string, newLayer: Layer) { - if (layerId === newLayer.id) { - this.updateLayer(newLayer); - return; - } - const vl = this.getVectorLayer(); - const parent = LayerUtil.findParent(vl, layerId).clone(); - const layerIndex = _.findIndex(parent.children, l => l.id === layerId); - const children = [...parent.children]; - children.splice(layerIndex, 1, newLayer); - parent.children = children; - const actions: Action[] = [ - new SetVectorLayer(LayerUtil.updateLayer(vl, parent)), - ...this.buildCleanupLayerIdActions(layerId), - ]; - const animation = this.getAnimation(); - const oldLayerBlocks = animation.blocks.filter(b => b.layerId === layerId); - const newAnimatableProperties = new Set(newLayer.animatableProperties.keys()); - // Preserve any blocks that are still animatable with the new layer. - const newLayerBlocks = oldLayerBlocks - .filter(b => newAnimatableProperties.has(b.propertyName)) - .map(b => { - b = b.clone(); - b.layerId = newLayer.id; - return b; - }); - const newAnimation = animation.clone(); - newAnimation.blocks = [ - ...animation.blocks.filter(b => b.layerId !== layerId), - ...newLayerBlocks, - ]; - actions.push(new SetAnimation(newAnimation)); - this.store.dispatch(new BatchAction(...actions)); - } - - /** - * Merges the specified group layer into its children layers. - * TODO: make it possible to merge groups that contain animation blocks? - */ - flattenGroupLayer(layerId: string) { - const vl = this.getVectorLayer(); - const layer = vl.findLayerById(layerId) as GroupLayer; - if (!layer.children.length) { - return; - } - const layerTransform = Matrix.flatten(LayerUtil.getCanvasTransformsForGroupLayer(layer)); - const layerChildren = layer.children.map( - (l: GroupLayer | PathLayer | ClipPathLayer): Layer => { - if (l instanceof GroupLayer) { - const flattenedTransform = Matrix.flatten([ - layerTransform, - ...LayerUtil.getCanvasTransformsForGroupLayer(l), - ]); - const { sx, sy } = flattenedTransform.getScaling(); - const degrees = flattenedTransform.getRotation(); - const { tx, ty } = flattenedTransform.getTranslation(); - l = l.clone(); - l.pivotX = 0; - l.pivotY = 0; - l.translateX = tx; - l.translateY = ty; - l.rotation = degrees; - l.scaleX = sx; - l.scaleY = sy; - return l; - } - l = l.clone(); - if (l instanceof PathLayer && l.strokeWidth) { - const scaleFactor = layerTransform.getScaleFactor(); - const newStrokeWidth = l.strokeWidth * scaleFactor ? 1 / scaleFactor : 0; - l.strokeWidth = MathUtil.round(newStrokeWidth); - } - const path = l.pathData; - if (!path || !l.pathData.getPathString()) { - return l; - } - l.pathData = path - .mutate() - .transform(layerTransform) - .build(); - return l; - }, - ); - const layerChildrenIds = new Set(layerChildren.map(l => l.id)); - const parent = LayerUtil.findParent(vl, layerId).clone(); - const children = [...parent.children]; - children.splice(_.findIndex(parent.children, l => l.id === layerId), 1, ...layerChildren); - parent.children = children; - const actions: Action[] = [ - new SetVectorLayer(LayerUtil.updateLayer(vl, parent)), - ...this.buildCleanupLayerIdActions(layerId), - ]; - const newAnimation = this.getAnimation().clone(); - // TODO: show a dialog if the user is about to unknowingly delete any blocks? - newAnimation.blocks = newAnimation.blocks.filter(b => b.layerId !== layerId); - // TODO: also attempt to merge children group animation blocks? - newAnimation.blocks = newAnimation.blocks.map(b => { - if (!(b instanceof PathAnimationBlock) || !layerChildrenIds.has(b.layerId)) { - return b; - } - const block = b.clone(); - if (block.fromValue) { - block.fromValue = block.fromValue - .mutate() - .transform(layerTransform) - .build(); - } - if (block.toValue) { - block.toValue = block.toValue - .mutate() - .transform(layerTransform) - .build(); - } - return block; - }); - actions.push(new SetAnimation(newAnimation)); - this.store.dispatch(new BatchAction(...actions)); - } - - private buildCleanupLayerIdActions(...deletedLayerIds: string[]) { - const collapsedLayerIds = this.getCollapsedLayerIds(); - const hiddenLayerIds = this.getHiddenLayerIds(); - const selectedLayerIds = this.getSelectedLayerIds(); - const differenceFn = (s: ReadonlySet, a: string[]) => - new Set(_.difference(Array.from(s), a)); - const actions: Action[] = []; - if (deletedLayerIds.some(id => collapsedLayerIds.has(id))) { - actions.push(new SetCollapsedLayers(differenceFn(collapsedLayerIds, deletedLayerIds))); - } - if (deletedLayerIds.some(id => hiddenLayerIds.has(id))) { - actions.push(new SetHiddenLayers(differenceFn(hiddenLayerIds, deletedLayerIds))); - } - if (deletedLayerIds.some(id => selectedLayerIds.has(id))) { - actions.push(new SetSelectedLayers(differenceFn(selectedLayerIds, deletedLayerIds))); - } - return actions; - } - - /** - * Groups or ungroups the selected layers. - */ - groupOrUngroupSelectedLayers(shouldGroup: boolean) { - let selectedLayerIds = this.getSelectedLayerIds(); - if (!selectedLayerIds.size) { - return; - } - let vl = this.getVectorLayer(); - - // Sort selected layers by order they appear in tree. - let tempSelLayers = Array.from(selectedLayerIds).map(id => vl.findLayerById(id)); - const selLayerOrdersMap: Dictionary = {}; - let n = 0; - vl.walk(layer => { - if (_.find(tempSelLayers, l => l.id === layer.id)) { - selLayerOrdersMap[layer.id] = n; - n++; - } - }); - tempSelLayers.sort((a, b) => selLayerOrdersMap[a.id] - selLayerOrdersMap[b.id]); - - if (shouldGroup) { - // Remove any layers that are descendants of other selected layers, - // and remove the vectorLayer itself if selected. - tempSelLayers = tempSelLayers.filter(layer => { - if (layer instanceof VectorLayer) { - return false; - } - let p = LayerUtil.findParent(vl, layer.id); - while (p) { - if (_.find(tempSelLayers, l => l.id === p.id)) { - return false; - } - p = LayerUtil.findParent(vl, p.id); - } - return true; - }); - - if (!tempSelLayers.length) { - return; - } - - // Find destination parent and insertion point. - const firstSelectedLayerParent = LayerUtil.findParent(vl, tempSelLayers[0].id).clone(); - const firstSelectedLayerIndexInParent = _.findIndex( - firstSelectedLayerParent.children, - l => l.id === tempSelLayers[0].id, - ); - - // Remove all selected items from their parents and move them into a new parent. - const newGroup = new GroupLayer({ - name: LayerUtil.getUniqueLayerName([vl], 'group'), - children: tempSelLayers, - }); - const parentChildren = [...firstSelectedLayerParent.children]; - parentChildren.splice(firstSelectedLayerIndexInParent, 0, newGroup); - _.remove(parentChildren, child => - _.find(tempSelLayers, selectedLayer => selectedLayer.id === child.id), - ); - firstSelectedLayerParent.children = parentChildren; - vl = LayerUtil.updateLayer(vl, firstSelectedLayerParent); - selectedLayerIds = new Set([newGroup.id]); - } else { - // Ungroup selected groups layers. - const newSelectedLayers: Layer[] = []; - tempSelLayers.filter(layer => layer instanceof GroupLayer).forEach(groupLayer => { - // Move children into parent. - const parent = LayerUtil.findParent(vl, groupLayer.id).clone(); - const indexInParent = Math.max( - 0, - _.findIndex(parent.children, l => l.id === groupLayer.id), - ); - const newChildren = [...parent.children]; - newChildren.splice(indexInParent, 0, ...groupLayer.children); - parent.children = newChildren; - vl = LayerUtil.updateLayer(vl, parent); - newSelectedLayers.splice(0, 0, ...groupLayer.children); - // Delete the parent. - vl = LayerUtil.removeLayers(vl, groupLayer.id); - }); - selectedLayerIds = new Set(newSelectedLayers.map(l => l.id)); - } - this.store.dispatch( - new BatchAction(new SetVectorLayer(vl), new SetSelectedLayers(selectedLayerIds)), - ); - } - - deleteSelectedModels() { - return this.store.dispatch(new BatchAction(...this.getDeleteSelectedModelsActions())); - } - - getDeleteSelectedModelsActions(): ReadonlyArray { - const collapsedLayerIds = this.getCollapsedLayerIds(); - const hiddenLayerIds = this.getHiddenLayerIds(); - const selectedLayerIds = this.getSelectedLayerIds(); - - let vl = this.getVectorLayer(); - if (selectedLayerIds.has(vl.id)) { - vl = new VectorLayer(); - collapsedLayerIds.clear(); - hiddenLayerIds.clear(); - } else { - selectedLayerIds.forEach(layerId => { - vl = LayerUtil.removeLayers(vl, layerId); - collapsedLayerIds.delete(layerId); - hiddenLayerIds.delete(layerId); - }); - } - - let animation = this.getAnimation(); - if (this.isAnimationSelected()) { - animation = new Animation(); - } - - const selectedBlockIds = this.getSelectedBlockIds(); - if (selectedBlockIds.size) { - animation = animation.clone(); - animation.blocks = animation.blocks.filter(b => !selectedBlockIds.has(b.id)); - } - - // Remove any blocks corresponding to deleted layers. - const filteredBlocks = animation.blocks.filter(b => !!vl.findLayerById(b.layerId)); - if (filteredBlocks.length !== animation.blocks.length) { - animation = animation.clone(); - animation.blocks = filteredBlocks; - } - - return [ - new SetVectorLayer(vl), - new SetCollapsedLayers(collapsedLayerIds), - new SetHiddenLayers(hiddenLayerIds), - new SetSelectedLayers(new Set()), - new SelectAnimation(false), - new SetAnimation(animation), - new SetSelectedBlocks(new Set()), - ]; - } - - updateBlocks(blocks: ReadonlyArray) { - if (!blocks.length) { - return; - } - const animation = this.getAnimation().clone(); - animation.blocks = animation.blocks.map(block => { - const newBlock = _.find(blocks, b => block.id === b.id); - return newBlock ? newBlock : block; - }); - this.store.dispatch(new SetAnimation(animation)); - } - - addBlocks( - blocks: Array<{ - id?: string; - layerId: string; - propertyName: string; - fromValue: any; - toValue: any; - currentTime: number; - duration?: number; - interpolator?: string; - }>, - autoSelectBlocks = true, - ) { - blocks.forEach(b => { - if (!b.id) { - b.id = _.uniqueId(); - } - }); - let animation = this.getAnimation(); - const addedBlocks: { id?: string }[] = []; - for (const block of blocks) { - const anim = this.addBlockToAnimation(animation, block); - if (animation !== anim) { - animation = anim; - addedBlocks.push(block); - } - } - this.store.dispatch( - new BatchAction( - new SetAnimation(animation), - new SelectAnimation(false), - new SetSelectedBlocks(new Set(addedBlocks.map(b => b.id))), - new SetSelectedLayers(new Set()), - ), - ); - } - - private addBlockToAnimation( - animation: Animation, - block: { - id?: string; - layerId: string; - propertyName: string; - fromValue: any; - toValue: any; - currentTime: number; - duration?: number; - interpolator?: string; - }, - ) { - const layer = this.getVectorLayer().findLayerById(block.layerId); - if (!layer) { - return animation; - } - const newBlockDuration = block.duration || 100; - const interpolator = block.interpolator || INTERPOLATORS[0].value; - const propertyName = block.propertyName; - const currentTime = block.currentTime; - - // Find the right start time for the block, which should be a gap between - // neighboring blocks closest to the active time cursor, of a minimum size. - const blocksByLayerId = ModelUtil.getOrderedBlocksByPropertyByLayer(animation); - const blockNeighbors = (blocksByLayerId[layer.id] || {})[propertyName] || []; - let gaps: Array<{ start: number; end: number }> = []; - for (let i = 0; i < blockNeighbors.length; i++) { - gaps.push({ - start: i === 0 ? 0 : blockNeighbors[i - 1].endTime, - end: blockNeighbors[i].startTime, - }); - } - gaps.push({ - start: blockNeighbors.length ? blockNeighbors[blockNeighbors.length - 1].endTime : 0, - end: animation.duration, - }); - gaps = gaps - .filter(gap => gap.end - gap.start >= newBlockDuration) - .map(gap => { - const dist = Math.min(Math.abs(gap.end - currentTime), Math.abs(gap.start - currentTime)); - return { ...gap, dist }; - }) - .sort((a, b) => a.dist - b.dist); - - if (!gaps.length) { - // No available gaps, cancel. - // TODO: show a disabled button to prevent this case? - console.warn('Ignoring failed attempt to add animation block'); - return animation; - } - - let startTime = Math.max(currentTime, gaps[0].start); - const endTime = Math.min(startTime + newBlockDuration, gaps[0].end); - if (endTime - startTime < newBlockDuration) { - startTime = endTime - newBlockDuration; - } - - // Generate the new block. - const property = layer.animatableProperties.get(propertyName); - let type: 'path' | 'color' | 'number'; - if (property.getTypeName() === 'PathProperty') { - type = 'path'; - } else if (property.getTypeName() === 'ColorProperty') { - type = 'color'; - } else { - type = 'number'; - } - - // TODO: clone the current rendered property value and set the from/to values appropriately - // const valueAtCurrentTime = - // this.studioState_.animationRenderer - // .getLayerPropertyValue(layer.id, propertyName); - - const newBlock = AnimationBlock.from({ - id: block.id ? block.id : undefined, - layerId: layer.id, - propertyName, - startTime, - endTime, - fromValue: block.fromValue, - toValue: block.toValue, - interpolator, - type, - }); - animation = animation.clone(); - animation.blocks = [...animation.blocks, newBlock]; - return animation; - } - - getVectorLayer() { - return this.queryStore(getVectorLayer); - } - - getSelectedLayerIds() { - return new Set(this.queryStore(getSelectedLayerIds)); - } - - getSelectedLayers() { - const vl = this.getVectorLayer(); - return Array.from(this.getSelectedLayerIds()).map(id => vl.findLayerById(id)); - } - - private getHiddenLayerIds() { - return new Set(this.queryStore(getHiddenLayerIds)); - } - - private getCollapsedLayerIds() { - return new Set(this.queryStore(getCollapsedLayerIds)); - } - - private getSelectedBlockIds() { - return new Set(this.queryStore(getSelectedBlockIds)); - } - - getSelectedBlocks() { - const anim = this.getAnimation(); - const blockIds = this.getSelectedBlockIds(); - return Array.from(blockIds).map(id => _.find(anim.blocks, b => b.id === id)); - } - - getAnimation() { - return this.queryStore(getAnimation); - } - - isAnimationSelected() { - return this.queryStore(isAnimationSelected); - } - - private queryStore(selector: OutputSelector T>) { - let obj: T; - this.store - .select(selector) - .pipe(first()) - .subscribe(o => (obj = o)); - return obj; - } -} diff --git a/src/app/pages/editor/services/paper.service.ts b/src/app/pages/editor/services/paper.service.ts deleted file mode 100644 index 453b2403..00000000 --- a/src/app/pages/editor/services/paper.service.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { Injectable, NgZone } from '@angular/core'; -import { VectorLayer } from 'app/pages/editor/model/layers'; -import { CursorType, ToolMode } from 'app/pages/editor/model/paper'; -import { Point } from 'app/pages/editor/scripts/common'; -import { Action, State, Store } from 'app/pages/editor/store'; -import { BatchAction } from 'app/pages/editor/store/batch/actions'; -import { getHiddenLayerIds, getSelectedLayerIds } from 'app/pages/editor/store/layers/selectors'; -import { - CreatePathInfo, - EditPathInfo, - RotateItemsInfo, - SetCreatePathInfo, - SetCursorType, - SetEditPathInfo, - SetHoveredLayerId, - SetRotateItemsInfo, - SetSelectionBox, - SetSnapGuideInfo, - SetSplitCurveInfo, - SetToolMode, - SetTooltipInfo, - SetTransformPathsInfo, - SetZoomPanInfo, - SnapGuideInfo, - SplitCurveInfo, - TooltipInfo, - TransformPathsInfo, - ZoomPanInfo, -} from 'app/pages/editor/store/paper/actions'; -import { - getCreatePathInfo, - getCursorType, - getEditPathInfo, - getHoveredLayerId, - getRotateItemsInfo, - getSelectionBox, - getSnapGuideInfo, - getSplitCurveInfo, - getToolMode, - getToolPanelState, - getTooltipInfo, - getTransformPathsInfo, - getZoomPanInfo, -} from 'app/pages/editor/store/paper/selectors'; -import { getAnimatedVectorLayer } from 'app/pages/editor/store/playback/selectors'; -import * as _ from 'lodash'; -import { OutputSelector } from 'reselect'; -import { first } from 'rxjs/operators'; - -import { LayerTimelineService } from './layertimeline.service'; - -/** A simple service that provides an interface for making paper.js changes to the store. */ -@Injectable({ providedIn: 'root' }) -export class PaperService { - constructor( - private readonly layerTimelineService: LayerTimelineService, - private readonly store: Store, - // TODO: figure out if this is the most efficient use of NgZone... - // TODO: can we get away with only executing in NgZone for certain dispatch store ops? - private readonly ngZone: NgZone, - ) {} - - observeToolPanelState() { - return this.store.select(getToolPanelState); - } - - enterDefaultMode() { - this.setToolMode(ToolMode.Default); - this.setEditPathInfo(undefined); - this.setRotateItemsInfo(undefined); - this.setTransformPathsInfo(undefined); - } - - enterEditPathMode() { - this.setToolMode(ToolMode.Default); - this.setEditPathInfo({ - selectedSegments: new Set(), - visibleHandleIns: new Set(), - visibleHandleOuts: new Set(), - selectedHandleIn: undefined, - selectedHandleOut: undefined, - }); - this.setRotateItemsInfo(undefined); - this.setTransformPathsInfo(undefined); - this.setCursorType(CursorType.PenAdd); - } - - /** Exits edit path mode. */ - exitEditPathMode() { - this.dispatchStore(new BatchAction(...this.getExitEditPathModeActions())); - } - - /** Returns a list of actions that will exit edit path mode. */ - getExitEditPathModeActions(): ReadonlyArray { - return [new SetEditPathInfo(undefined), ...this.getClearEditPathModeStateActions()]; - } - - /** Returns a list of actions that will clear any state associated with edit path mode. */ - getClearEditPathModeStateActions(): ReadonlyArray { - return [ - new SetCreatePathInfo(undefined), - new SetSplitCurveInfo(undefined), - new SetSnapGuideInfo(undefined), - new SetCursorType(CursorType.Default), - ]; - } - - enterRotateItemsMode() { - this.setToolMode(ToolMode.Default); - this.setEditPathInfo(undefined); - this.setRotateItemsInfo({}); - this.setTransformPathsInfo(undefined); - } - - enterTransformPathsMode() { - this.setToolMode(ToolMode.Default); - this.setEditPathInfo(undefined); - this.setRotateItemsInfo(undefined); - this.setTransformPathsInfo({}); - } - - enterPencilMode() { - this.setToolMode(ToolMode.Pencil); - this.setCursorType(CursorType.Pencil); - } - - enterCreateRectangleMode() { - this.setToolMode(ToolMode.Rectangle); - this.setCursorType(CursorType.Crosshair); - } - - enterCreateEllipseMode() { - this.setToolMode(ToolMode.Ellipse); - this.setCursorType(CursorType.Crosshair); - } - - setVectorLayer(vl: VectorLayer) { - // TODO: avoid running in angular zone whenever possible? - this.ngZone.run(() => this.layerTimelineService.setVectorLayer(vl)); - } - - getVectorLayer() { - // TODO: return the non-animated vector layer here (using layer timeline service) instead? - return this.queryStore(getAnimatedVectorLayer).vl; - } - - setSelectedLayerIds(layerIds: Set) { - if (!_.isEqual(this.queryStore(getSelectedLayerIds), layerIds)) { - this.ngZone.run(() => this.layerTimelineService.setSelectedLayers(layerIds)); - } - } - - getSelectedLayerIds() { - return this.queryStore(getSelectedLayerIds); - } - - setHoveredLayerId(layerId: string | undefined) { - if (this.queryStore(getHoveredLayerId) !== layerId) { - this.ngZone.run(() => this.store.dispatch(new SetHoveredLayerId(layerId))); - } - } - - getHoveredLayerId() { - return this.queryStore(getHoveredLayerId); - } - - getHiddenLayerIds() { - return this.queryStore(getHiddenLayerIds); - } - - setSelectionBox(box: { from: Point; to: Point } | undefined) { - if (!_.isEqual(this.queryStore(getSelectionBox), box)) { - // TODO: run this outside angular zone instead? - this.dispatchStore(new SetSelectionBox(box)); - } - } - - getSelectionBox() { - return this.queryStore(getSelectionBox); - } - - setCreatePathInfo(info: CreatePathInfo | undefined) { - if (!_.isEqual(this.queryStore(getCreatePathInfo), info)) { - this.dispatchStore(new SetCreatePathInfo(info)); - } - } - - getCreatePathInfo() { - return this.queryStore(getCreatePathInfo); - } - - setSplitCurveInfo(info: SplitCurveInfo | undefined) { - if (!_.isEqual(this.queryStore(getSplitCurveInfo), info)) { - this.dispatchStore(new SetSplitCurveInfo(info)); - } - } - - setToolMode(toolMode: ToolMode) { - if (!_.isEqual(this.queryStore(getToolMode), toolMode)) { - this.dispatchStore(new SetToolMode(toolMode)); - } - } - - getToolMode() { - return this.queryStore(getToolMode); - } - - setEditPathInfo(info: EditPathInfo | undefined) { - if (!_.isEqual(this.queryStore(getEditPathInfo), info)) { - this.dispatchStore(new SetEditPathInfo(info)); - } - } - - getEditPathInfo() { - return this.queryStore(getEditPathInfo); - } - - setRotateItemsInfo(info: RotateItemsInfo | undefined) { - if (!_.isEqual(this.queryStore(getRotateItemsInfo), info)) { - this.dispatchStore(new SetRotateItemsInfo(info)); - } - } - - getRotateItemsInfo() { - return this.queryStore(getRotateItemsInfo); - } - - setTransformPathsInfo(info: TransformPathsInfo | undefined) { - if (!_.isEqual(this.queryStore(getTransformPathsInfo), info)) { - this.dispatchStore(new SetTransformPathsInfo(info)); - } - } - - getTransformPathsInfo() { - return this.queryStore(getTransformPathsInfo); - } - - setCursorType(cursorType: CursorType) { - if (!_.isEqual(this.queryStore(getCursorType), cursorType)) { - this.dispatchStore(new SetCursorType(cursorType)); - } - } - - setSnapGuideInfo(info: SnapGuideInfo | undefined) { - if (!_.isEqual(this.queryStore(getSnapGuideInfo), info)) { - this.dispatchStore(new SetSnapGuideInfo(info)); - } - } - - setZoomPanInfo(info: ZoomPanInfo) { - if (!_.isEqual(this.queryStore(getZoomPanInfo), info)) { - this.dispatchStore(new SetZoomPanInfo(info)); - } - } - - setTooltipInfo(info: TooltipInfo | undefined) { - if (!_.isEqual(this.queryStore(getTooltipInfo), info)) { - this.dispatchStore(new SetTooltipInfo(info)); - } - } - - deleteSelectedModels() { - if (this.getRotateItemsInfo() || this.getTransformPathsInfo()) { - // Do not delete layers when in rotate items or transform paths mode. - return; - } - this.layerTimelineService.deleteSelectedModels(); - } - - getDeleteSelectedModelsActions() { - return this.layerTimelineService.getDeleteSelectedModelsActions(); - } - - dispatchStore(action: Action) { - if (NgZone.isInAngularZone()) { - this.store.dispatch(action); - } else { - // PaperService methods are usually executed outside of the Angular zone - // (since they originate from event handlers registered by paper.js). In - // order to ensure change detection works properly, we need to force - // state changes to be executed inside the Angular zone. - this.ngZone.run(() => this.store.dispatch(action)); - } - } - - private queryStore(selector: OutputSelector T>) { - let obj: T; - this.store - .select(selector) - .pipe(first()) - .subscribe(o => (obj = o)); - return obj; - } -} diff --git a/src/app/pages/editor/services/playback.service.ts b/src/app/pages/editor/services/playback.service.ts deleted file mode 100644 index 04383478..00000000 --- a/src/app/pages/editor/services/playback.service.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { Injectable, NgZone } from '@angular/core'; -import { Action, State, Store } from 'app/pages/editor/store'; -import { BatchAction } from 'app/pages/editor/store/batch/actions'; -import { - SetCurrentTime, - SetIsPlaying, - SetIsRepeating, - SetIsSlowMotion, -} from 'app/pages/editor/store/playback/actions'; -import { - getAnimatedVectorLayer, - getCurrentTime, - getIsPlaying, - getIsRepeating, - getIsSlowMotion, -} from 'app/pages/editor/store/playback/selectors'; -import { getAnimation } from 'app/pages/editor/store/timeline/selectors'; -import * as _ from 'lodash'; -import { OutputSelector } from 'reselect'; -import { first } from 'rxjs/operators'; - -/** A simple service that provides an interface for making playback changes. */ -@Injectable({ providedIn: 'root' }) -export class PlaybackService { - private readonly animator: Animator; - - // TODO: set current time to 0 when animation/vector layer changes (like before)? - // TODO: reset time (or any other special handling) during workspace resets? - constructor(private readonly store: Store, ngZone: NgZone) { - this.animator = new Animator(ngZone, { - onAnimationStart: () => { - this.setIsPlaying(true); - }, - onAnimationUpdate: (currentTime: number) => { - currentTime = Math.round(currentTime); - this.store.dispatch(new SetCurrentTime(currentTime)); - }, - onAnimationEnd: () => { - this.setIsPlaying(false); - }, - }); - this.store.select(getIsPlaying).subscribe(isPlaying => { - if (isPlaying) { - const { duration } = this.queryStore(getAnimation); - const currentTime = this.getCurrentTime(); - const startTime = duration === this.getCurrentTime() ? 0 : currentTime; - this.animator.play(duration, startTime); - } else { - this.animator.pause(); - } - }); - this.store.select(getIsSlowMotion).subscribe(isSlowMotion => { - this.animator.setIsSlowMotion(isSlowMotion); - }); - this.store.select(getIsRepeating).subscribe(isRepeating => { - this.animator.setIsRepeating(isRepeating); - }); - } - - asObservable() { - return this.store.select(getAnimatedVectorLayer); - } - - getCurrentTime() { - return this.queryStore(getCurrentTime); - } - - setCurrentTime(currentTime: number) { - currentTime = Math.round(currentTime); - if (this.queryStore(getCurrentTime) !== currentTime) { - this.store.dispatch(new SetCurrentTime(currentTime)); - } - } - - // TODO: make it so rewind navigates to the start of the currently active block? - rewind() { - const actions: Action[] = []; - if (this.getCurrentTime() !== 0) { - actions.push(new SetCurrentTime(0)); - } - if (this.queryStore(getIsPlaying)) { - actions.push(new SetIsPlaying(false)); - } - if (actions.length) { - this.store.dispatch(new BatchAction(...actions)); - } - } - - // TODO: make it so fast forward navigates to the end of the currently active block? - fastForward() { - const actions: Action[] = []; - const { duration } = this.queryStore(getAnimation); - if (this.getCurrentTime() !== duration) { - actions.push(new SetCurrentTime(duration)); - } - if (this.queryStore(getIsPlaying)) { - actions.push(new SetIsPlaying(false)); - } - if (actions.length) { - this.store.dispatch(new BatchAction(...actions)); - } - } - - toggleIsSlowMotion() { - this.store.dispatch(new SetIsSlowMotion(!this.queryStore(getIsSlowMotion))); - } - - toggleIsRepeating() { - this.store.dispatch(new SetIsRepeating(!this.queryStore(getIsRepeating))); - } - - toggleIsPlaying() { - this.setIsPlaying(!this.queryStore(getIsPlaying)); - } - - private setIsPlaying(isPlaying: boolean) { - if (isPlaying !== this.queryStore(getIsPlaying)) { - this.store.dispatch(new SetIsPlaying(isPlaying)); - } - } - - private queryStore(selector: OutputSelector T>) { - let obj: T; - this.store - .select(selector) - .pipe(first()) - .subscribe(o => (obj = o)); - return obj; - } -} - -const REPEAT_DELAY = 750; -const DEFAULT_PLAYBACK_SPEED = 1; -const SLOW_MOTION_PLAYBACK_SPEED = 5; - -/** A simple class that simulates an animation loop. */ -class Animator { - private timeoutId: number; - private animationFrameId: number; - private playbackSpeed = DEFAULT_PLAYBACK_SPEED; - private isRepeating = false; - - constructor(private readonly ngZone: NgZone, private readonly callback: Callback) {} - - setIsRepeating(isRepeating: boolean) { - this.isRepeating = isRepeating; - } - - setIsSlowMotion(isSlowMotion: boolean) { - // TODO: make it possible to change this mid-animation? - this.playbackSpeed = isSlowMotion ? SLOW_MOTION_PLAYBACK_SPEED : DEFAULT_PLAYBACK_SPEED; - } - - play(duration: number, startTime: number) { - this.runOutsideAngular(() => this.startAnimation(duration, startTime)); - this.runInsideAngular(() => this.callback.onAnimationStart()); - } - - private startAnimation(duration: number, startTime: number) { - let startTimestamp: number; - const playbackSpeed = this.playbackSpeed; - const onAnimationFrameFn = (timestamp: number) => { - if (!startTimestamp) { - startTimestamp = timestamp; - } - const progress = timestamp - startTimestamp + startTime; - if (progress < duration * playbackSpeed) { - this.animationFrameId = window.requestAnimationFrame(onAnimationFrameFn); - } else if (this.isRepeating) { - this.timeoutId = window.setTimeout( - () => this.startAnimation(duration, startTime), - REPEAT_DELAY, - ); - } else { - this.pause(true); - } - const fraction = _.clamp(progress / (duration * playbackSpeed), 0, 1); - const executeFn = () => this.callback.onAnimationUpdate(fraction * duration); - if (fraction === 0 || fraction === 1) { - this.runInsideAngular(executeFn); - } else { - executeFn(); - } - }; - this.animationFrameId = window.requestAnimationFrame(onAnimationFrameFn); - } - - pause(shouldNotify = false) { - if (this.timeoutId) { - window.clearTimeout(this.timeoutId); - this.timeoutId = undefined; - } - if (this.animationFrameId) { - window.cancelAnimationFrame(this.animationFrameId); - this.animationFrameId = undefined; - } - if (shouldNotify) { - this.runInsideAngular(() => this.callback.onAnimationEnd()); - } - } - - rewind() { - this.pause(); - } - - fastForward() { - this.pause(); - } - - private runInsideAngular(fn: () => void) { - if (NgZone.isInAngularZone()) { - fn(); - } else { - this.ngZone.run(fn); - } - } - - private runOutsideAngular(fn: () => void) { - if (NgZone.isInAngularZone()) { - this.ngZone.runOutsideAngular(fn); - } else { - fn(); - } - } -} - -interface Callback { - onAnimationStart(): void; - onAnimationUpdate(currentTime: number): void; - onAnimationEnd(): void; -} diff --git a/src/app/pages/editor/services/shortcut.service.ts b/src/app/pages/editor/services/shortcut.service.ts deleted file mode 100644 index 94016973..00000000 --- a/src/app/pages/editor/services/shortcut.service.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { Injectable } from '@angular/core'; -import { State, Store } from 'app/pages/editor/store'; -import { environment } from 'environments/environment'; -import * as $ from 'jquery'; -import { ActionCreators } from 'redux-undo'; -import { Subject } from 'rxjs'; - -import { ActionModeService } from './actionmode.service'; -import { LayerTimelineService } from './layertimeline.service'; -import { PlaybackService } from './playback.service'; - -export enum Shortcut { - ZoomToFit = 1, -} - -interface ModifierKeyEvent { - readonly metaKey: boolean; - readonly ctrlKey?: boolean; -} - -@Injectable({ providedIn: 'root' }) -export class ShortcutService { - private isInit = false; - private readonly shortcutSubject = new Subject(); - - /** Returns true if the event is a modifier key (meta for Macs, ctrl for others). */ - static isOsDependentModifierKey(event: ModifierKeyEvent) { - return !!(ShortcutService.isMac() ? !!event.metaKey : !!event.ctrlKey); - } - - private static isMac() { - return navigator.appVersion.includes('Mac'); - } - - constructor( - private readonly store: Store, - private readonly actionModeService: ActionModeService, - private readonly playbackService: PlaybackService, - private readonly layerTimelineService: LayerTimelineService, - ) {} - - asObservable() { - return this.shortcutSubject.asObservable(); - } - - init() { - if (this.isInit) { - return; - } - this.isInit = true; - - $(window).on('keydown', event => { - if (ShortcutService.isOsDependentModifierKey(event)) { - if (event.keyCode === 'Z'.charCodeAt(0)) { - this.store.dispatch(event.shiftKey ? ActionCreators.redo() : ActionCreators.undo()); - return false; - } - if (event.keyCode === 'G'.charCodeAt(0)) { - this.layerTimelineService.groupOrUngroupSelectedLayers(!event.shiftKey); - return false; - } - if (event.keyCode === 'O'.charCodeAt(0)) { - this.shortcutSubject.next(Shortcut.ZoomToFit); - return false; - } - } - if (event.ctrlKey || event.metaKey) { - // Do nothing if the ctrl or meta keys are pressed. - return undefined; - } - if (document.activeElement.matches('input')) { - // Ignore shortcuts when an input element has focus. - return true; - } - if (event.keyCode === 8 || event.keyCode === 46) { - // Backspace or delete. - const isActionMode = this.actionModeService.isActionMode(); - // If we aren't in beta or it is action mode, handle the backspace/delete - // event here. Otherwise we will handle it in the gesture tool (which is - // where we will likely want to move all of the shortcut logic in the future). - if (!environment.beta || isActionMode) { - // In case there's a JS error, never navigate away. - event.preventDefault(); - if (isActionMode) { - this.actionModeService.deleteSelectedActionModeModels(); - } else { - this.layerTimelineService.deleteSelectedModels(); - } - return false; - } - } - if (event.keyCode === 27) { - // Escape. - this.actionModeService.closeActionMode(); - return false; - } - // TODO: figure out how to re-enable this keyboard shortcut in beta - if (!environment.beta && event.keyCode === 32) { - // Spacebar. - this.playbackService.toggleIsPlaying(); - return false; - } - if (event.keyCode === 37) { - // Left arrow. - this.playbackService.rewind(); - return false; - } - if (event.keyCode === 39) { - // Right arrow. - this.playbackService.fastForward(); - return false; - } - if (event.keyCode === 'R'.charCodeAt(0)) { - if (this.actionModeService.isShowingSubPathActionMode()) { - this.actionModeService.reverseSelectedSubPaths(); - } else { - this.playbackService.toggleIsRepeating(); - } - return false; - } - if (event.keyCode === 'S'.charCodeAt(0)) { - if ( - this.actionModeService.isShowingSubPathActionMode() || - this.actionModeService.isShowingSegmentActionMode() - ) { - this.actionModeService.toggleSplitSubPathsMode(); - } else { - this.playbackService.toggleIsSlowMotion(); - } - return false; - } - if (event.keyCode === 'A'.charCodeAt(0)) { - if ( - this.actionModeService.isShowingSubPathActionMode() || - this.actionModeService.isShowingSegmentActionMode() - ) { - this.actionModeService.toggleSplitCommandsMode(); - } else if (this.actionModeService.isShowingPointActionMode()) { - this.actionModeService.splitSelectedPointInHalf(); - } - return false; - } - if (event.keyCode === 'D'.charCodeAt(0)) { - if ( - this.actionModeService.isShowingSubPathActionMode() || - this.actionModeService.isShowingSegmentActionMode() - ) { - this.actionModeService.togglePairSubPathsMode(); - } - return false; - } - if (event.keyCode === 'B'.charCodeAt(0)) { - if (this.actionModeService.isShowingSubPathActionMode()) { - this.actionModeService.shiftBackSelectedSubPaths(); - } - return false; - } - if (event.keyCode === 'F'.charCodeAt(0)) { - if (this.actionModeService.isShowingSubPathActionMode()) { - this.actionModeService.shiftForwardSelectedSubPaths(); - } else if (this.actionModeService.isShowingPointActionMode()) { - this.actionModeService.shiftPointToFront(); - } - return false; - } - return undefined; - }); - } - - destroy() { - if (!this.isInit) { - return; - } - this.isInit = false; - $(window).unbind('keydown'); - } - - getZoomToFitText() { - return `${this.getCmdOrCtrlText()} + O`; - } - - private getCmdOrCtrlText() { - return ShortcutService.isMac() ? 'Cmd' : 'Ctrl'; - } -} diff --git a/src/app/pages/editor/services/snackbar.service.ts b/src/app/pages/editor/services/snackbar.service.ts deleted file mode 100644 index 4c8b48d4..00000000 --- a/src/app/pages/editor/services/snackbar.service.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Injectable } from '@angular/core'; -import { MatSnackBar } from '@angular/material'; - -export enum Duration { - Short = 2750, - Long = 5000, -} - -@Injectable({ providedIn: 'root' }) -export class SnackBarService { - constructor(private readonly snackBar: MatSnackBar) {} - - show(message: string, action = '', duration = Duration.Short) { - this.snackBar.open(message, action.toUpperCase(), { duration }); - } -} diff --git a/src/app/pages/editor/services/theme.service.ts b/src/app/pages/editor/services/theme.service.ts deleted file mode 100644 index 843bfc04..00000000 --- a/src/app/pages/editor/services/theme.service.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Injectable } from '@angular/core'; -import { State, Store } from 'app/pages/editor/store'; -import { SetTheme } from 'app/pages/editor/store/theme/actions'; -import { ThemeType } from 'app/pages/editor/store/theme/reducer'; -import { getThemeType } from 'app/pages/editor/store/theme/selectors'; -import { first } from 'rxjs/operators'; - -// TODO: change the animation block green color - -const LIGHT_PRIMARY_TEXT = 'rgba(0, 0, 0, 0.87)'; -const DARK_PRIMARY_TEXT = 'rgba(255, 255, 255, 1)'; -const LIGHT_SECONDARY_TEXT = 'rgba(0, 0, 0, 0.54)'; -const DARK_SECONDARY_TEXT = 'rgba(255, 255, 255, 0.7)'; -const LIGHT_DISABLED_TEXT = 'rgba(0, 0, 0, 0.26)'; -const DARK_DISABLED_TEXT = 'rgba(255, 255, 255, 0.3)'; -const LIGHT_DIVIDER_TEXT = 'rgba(0, 0, 0, 0.12)'; -const DARK_DIVIDER_TEXT = 'rgba(255, 255, 255, 0.12)'; - -/** - * A simple service that provides an interface for making theme changes. - */ -@Injectable({ providedIn: 'root' }) -export class ThemeService { - constructor(private readonly store: Store) {} - - setTheme(themeType: ThemeType) { - this.store.dispatch(new SetTheme(themeType)); - } - - toggleTheme() { - this.setTheme(this.getThemeType().themeType === 'dark' ? 'light' : 'dark'); - } - - getThemeType() { - let result: { themeType: ThemeType; isInitialPageLoad: boolean }; - this.asObservable() - .pipe(first()) - .subscribe(res => (result = res)); - return result; - } - - asObservable() { - return this.store.select(getThemeType); - } - - getPrimaryTextColor() { - return this.getThemeType().themeType === 'dark' ? DARK_PRIMARY_TEXT : LIGHT_PRIMARY_TEXT; - } - - getSecondaryTextColor() { - return this.getThemeType().themeType === 'dark' ? DARK_SECONDARY_TEXT : LIGHT_SECONDARY_TEXT; - } - - getDisabledTextColor() { - return this.getThemeType().themeType === 'dark' ? DARK_DISABLED_TEXT : LIGHT_DISABLED_TEXT; - } - - getDividerTextColor() { - return this.getThemeType().themeType === 'dark' ? DARK_DIVIDER_TEXT : LIGHT_DIVIDER_TEXT; - } -} diff --git a/src/app/pages/editor/store/actionmode/actions.ts b/src/app/pages/editor/store/actionmode/actions.ts deleted file mode 100644 index 16fa3681..00000000 --- a/src/app/pages/editor/store/actionmode/actions.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { ActionMode, ActionSource, Hover, Selection } from 'app/pages/editor/model/actionmode'; -import { Action } from 'app/pages/editor/store'; - -export enum ActionModeActionTypes { - SetActionMode = '__actionmode__SET_ACTION_MODE', - SetActionModeHover = '__actionmode__SET_ACTION_MODE_HOVER', - SetActionModeSelections = '__actionmode__SET_ACTION_MODE_SELECTIONS', - SetPairedSubPaths = '__actionmode__SET_PAIRED_SUBPATHS', - SetUnpairedSubPath = '__actionmode__SET_UNPAIRED_SUBPATH', -} - -export class SetActionMode implements Action { - readonly type = ActionModeActionTypes.SetActionMode; - readonly payload: { mode: ActionMode }; - constructor(mode: ActionMode) { - this.payload = { mode }; - } -} - -export class SetActionModeHover implements Action { - readonly type = ActionModeActionTypes.SetActionModeHover; - readonly payload: { hover: Hover }; - constructor(hover: Hover) { - this.payload = { hover }; - } -} - -export class SetActionModeSelections implements Action { - readonly type = ActionModeActionTypes.SetActionModeSelections; - readonly payload: { selections: ReadonlyArray }; - constructor(selections: ReadonlyArray) { - this.payload = { selections }; - } -} - -export class SetPairedSubPaths implements Action { - readonly type = ActionModeActionTypes.SetPairedSubPaths; - readonly payload: { pairedSubPaths: ReadonlySet }; - constructor(pairedSubPaths: ReadonlySet) { - this.payload = { pairedSubPaths }; - } -} - -export class SetUnpairedSubPath implements Action { - readonly type = ActionModeActionTypes.SetUnpairedSubPath; - readonly payload: { unpairedSubPath: { source: ActionSource; subIdx: number } }; - constructor(unpairedSubPath: { source: ActionSource; subIdx: number }) { - this.payload = { unpairedSubPath }; - } -} - -export type ActionModeActions = - | SetActionMode - | SetActionModeHover - | SetActionModeSelections - | SetPairedSubPaths - | SetUnpairedSubPath; diff --git a/src/app/pages/editor/store/actionmode/reducer.ts b/src/app/pages/editor/store/actionmode/reducer.ts deleted file mode 100644 index b8d7f4b3..00000000 --- a/src/app/pages/editor/store/actionmode/reducer.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { ActionMode, ActionSource, Hover, Selection } from 'app/pages/editor/model/actionmode'; - -import { ActionModeActionTypes, ActionModeActions } from './actions'; - -export interface State { - readonly mode: ActionMode; - readonly hover: Hover; - readonly selections: ReadonlyArray; - readonly pairedSubPaths: ReadonlySet; - readonly unpairedSubPath: { readonly source: ActionSource; readonly subIdx: number }; -} - -export function buildInitialState() { - return { - mode: ActionMode.None, - hover: undefined, - selections: [], - pairedSubPaths: new Set(), - unpairedSubPath: undefined, - } as State; -} - -// TODO: move as much logic as possible from here into action mode service -export function reducer(state = buildInitialState(), action: ActionModeActions) { - switch (action.type) { - // Set the app mode during action mode. - case ActionModeActionTypes.SetActionMode: { - const { mode } = action.payload; - if (mode === ActionMode.None) { - return buildInitialState(); - } - let { selections, pairedSubPaths, unpairedSubPath } = state; - if (state.mode === ActionMode.PairSubPaths && mode !== state.mode) { - // Reset the paired subpath state when leaving pair subpath mode. - pairedSubPaths = new Set(); - unpairedSubPath = undefined; - } - if (mode === ActionMode.Selection && mode !== state.mode) { - // Clear selections when switching back to selection mode. - selections = []; - } - return { ...state, mode, selections, pairedSubPaths, unpairedSubPath }; - } - - // Set the hover mode during action mode. - case ActionModeActionTypes.SetActionModeHover: { - const { hover } = action.payload; - return { ...state, hover }; - } - - // Set the path selections during action mode. - case ActionModeActionTypes.SetActionModeSelections: { - const { selections } = action.payload; - return { ...state, selections }; - } - - // Set the currently paired subpaths. - case ActionModeActionTypes.SetPairedSubPaths: { - const pairedSubPaths = new Set(action.payload.pairedSubPaths); - return { ...state, pairedSubPaths }; - } - - // Set the currently unpaired subpath in pair subpaths mode. - case ActionModeActionTypes.SetUnpairedSubPath: { - const { unpairedSubPath } = action.payload; - return { ...state, unpairedSubPath }; - } - } - return state; -} diff --git a/src/app/pages/editor/store/actionmode/selectors.ts b/src/app/pages/editor/store/actionmode/selectors.ts deleted file mode 100644 index 3ad52323..00000000 --- a/src/app/pages/editor/store/actionmode/selectors.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { ActionMode, ActionSource, SelectionType } from 'app/pages/editor/model/actionmode'; -import { LayerUtil, MorphableLayer, VectorLayer } from 'app/pages/editor/model/layers'; -import { Animation, PathAnimationBlock } from 'app/pages/editor/model/timeline'; -import { ActionModeUtil } from 'app/pages/editor/scripts/actionmode'; -import { AnimationRenderer } from 'app/pages/editor/scripts/animator'; -import { - getHiddenLayerIds, - getSelectedLayerIds, - getVectorLayer, -} from 'app/pages/editor/store/layers/selectors'; -import { State } from 'app/pages/editor/store/reducer'; -import { createDeepEqualSelector, getEditorState } from 'app/pages/editor/store/selectors'; -import { - getAnimation, - getSingleSelectedBlockLayerId, - getSingleSelectedPathBlock, -} from 'app/pages/editor/store/timeline/selectors'; -import { createSelector, createStructuredSelector } from 'reselect'; - -const getActionModeState = createSelector(getEditorState, s => s.actionmode); -export const getActionMode = createSelector(getActionModeState, s => s.mode); -export const isActionMode = createSelector(getActionMode, mode => mode !== ActionMode.None); -export const getActionModeHover = createDeepEqualSelector(getActionModeState, s => s.hover); - -export const getActionModeSelections = createDeepEqualSelector( - getActionModeState, - s => s.selections, -); - -export const getActionModeSubPathSelections = createDeepEqualSelector( - getActionModeSelections, - selections => selections.filter(s => s.type === SelectionType.SubPath), -); -export const getActionModeSegmentSelections = createDeepEqualSelector( - getActionModeSelections, - selections => selections.filter(s => s.type === SelectionType.Segment), -); -export const getActionModePointSelections = createDeepEqualSelector( - getActionModeSelections, - selections => selections.filter(s => s.type === SelectionType.Point), -); - -export const getPairedSubPaths = createDeepEqualSelector( - getActionModeState, - state => state.pairedSubPaths, -); -export const getUnpairedSubPath = createDeepEqualSelector( - getActionModeState, - state => state.unpairedSubPath, -); - -function getVectorLayerValue(getTimeFn: (block: PathAnimationBlock) => number) { - return createSelector( - [getVectorLayer, getAnimation, getSingleSelectedPathBlock], - (vl, anim, block) => { - if (!block) { - return undefined; - } - // Note this is a bit dangerous because the renderer interpolates paths - // and that causes all mutated path state to be lost if we aren't careful. - // There are currently checks in PathProperty.ts to avoid this by returning - // the start and end path when the interpolated fraction is 0 and 1 respectively. - const renderer = new AnimationRenderer(vl, anim); - const timeMillis = getTimeFn(block); - // First interpolate the entire vector layer. - const renderedVl = renderer.setCurrentTime(timeMillis); - // TODO: this is hacky! the real solution is to not clear path state after interpolations - // Replace the interpolated value with the block's to/from value. - const layer = vl.findLayerById(block.layerId).clone() as MorphableLayer; - layer.pathData = timeMillis === block.startTime ? block.fromValue : block.toValue; - return LayerUtil.updateLayer(renderedVl, layer); - }, - ); -} - -const getVectorLayerFromValue = getVectorLayerValue(block => block.startTime); -const getVectorLayerToValue = getVectorLayerValue(block => block.endTime); - -type CombinerFunc = (vl: VectorLayer, anim: Animation, block: PathAnimationBlock) => VectorLayer; - -function getMorphableLayerValue( - selector: Reselect.OutputSelector, -) { - return createSelector([selector, getSingleSelectedBlockLayerId], (vl, blockLayerId) => { - if (!vl || !blockLayerId) { - return undefined; - } - return vl.findLayerById(blockLayerId) as MorphableLayer; - }); -} - -const getMorphableLayerFromValue = getMorphableLayerValue(getVectorLayerFromValue); -const getMorphableLayerToValue = getMorphableLayerValue(getVectorLayerToValue); - -const getPathsCompatibleResult = createSelector( - getSingleSelectedPathBlock, - block => (block ? ActionModeUtil.checkPathsCompatible(block) : undefined), -); - -function getHighlightedSubIdxWithError(actionSource: ActionSource) { - return createSelector( - [getActionMode, getActionModeSelections, getPathsCompatibleResult], - (mode, selections, result) => { - if (!result) { - // Then there is no path animation block currently selected. - return undefined; - } - const { areCompatible, errorPath, errorSubIdx } = result; - if (mode !== ActionMode.Selection || selections.length) { - // Don't show any highlights if we're not in selection mode, or - // if there are any existing selections. - return undefined; - } - if (areCompatible || errorPath !== actionSource || errorSubIdx === undefined) { - return undefined; - } - return errorSubIdx; - }, - ); -} - -const actionModeBaseSelectors = { - blockLayerId: getSingleSelectedBlockLayerId, - isActionMode, - hover: getActionModeHover, - selections: getActionModeSelections, - pairedSubPaths: getPairedSubPaths, - unpairedSubPath: getUnpairedSubPath, - hiddenLayerIds: getHiddenLayerIds, - selectedLayerIds: getSelectedLayerIds, -}; - -export const getActionModeStartState = createStructuredSelector({ - ...actionModeBaseSelectors, - vectorLayer: getVectorLayerFromValue, - subIdxWithError: getHighlightedSubIdxWithError(ActionSource.From), -}); - -export const getActionModeEndState = createStructuredSelector({ - ...actionModeBaseSelectors, - vectorLayer: getVectorLayerToValue, - subIdxWithError: getHighlightedSubIdxWithError(ActionSource.To), -}); - -export const getToolbarState = createStructuredSelector({ - mode: getActionMode, - fromMl: getMorphableLayerFromValue, - toMl: getMorphableLayerToValue, - selections: getActionModeSelections, - unpairedSubPath: getUnpairedSubPath, - block: getSingleSelectedPathBlock, -}); diff --git a/src/app/pages/editor/store/batch/actions.ts b/src/app/pages/editor/store/batch/actions.ts deleted file mode 100644 index 61e19f18..00000000 --- a/src/app/pages/editor/store/batch/actions.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Action } from 'app/pages/editor/store'; - -export enum BatchActionTypes { - BatchAction = '__batch__BATCH', -} - -export class BatchAction implements Action { - readonly type = BatchActionTypes.BatchAction; - readonly payload: ReadonlyArray; - constructor(...actions: Action[]) { - this.payload = actions; - } -} - -export type BatchActions = BatchAction; diff --git a/src/app/pages/editor/store/batch/metareducer.ts b/src/app/pages/editor/store/batch/metareducer.ts deleted file mode 100644 index c778e3f9..00000000 --- a/src/app/pages/editor/store/batch/metareducer.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ActionReducer } from 'app/pages/editor/store'; -import { EditorState } from 'app/pages/editor/store/reducer'; - -import { BatchActionTypes, BatchActions } from './actions'; - -export function metaReducer(reducer: ActionReducer): ActionReducer { - return (state: EditorState, action: BatchActions) => { - const isBatchAction = action.type === BatchActionTypes.BatchAction; - return (isBatchAction ? action.payload : [action]).reduce(reducer, state); - }; -} diff --git a/src/app/pages/editor/store/common/selectors.ts b/src/app/pages/editor/store/common/selectors.ts deleted file mode 100644 index aaab3324..00000000 --- a/src/app/pages/editor/store/common/selectors.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { getActionMode, isActionMode } from 'app/pages/editor/store/actionmode/selectors'; -import { - getCollapsedLayerIds, - getHiddenLayerIds, - getSelectedLayerIds, - getVectorLayer, -} from 'app/pages/editor/store/layers/selectors'; -import { getHoveredLayerId } from 'app/pages/editor/store/paper/selectors'; -import { isBeingReset } from 'app/pages/editor/store/reset/selectors'; -import { - getAnimation, - getSelectedBlockIds, - getSelectedBlockLayerIds, - getSingleSelectedPathBlock, - isAnimationSelected, -} from 'app/pages/editor/store/timeline/selectors'; -import { createSelector, createStructuredSelector } from 'reselect'; - -export const getCanvasOverlayState = createStructuredSelector({ - hiddenLayerIds: getHiddenLayerIds, - selectedLayerIds: getSelectedLayerIds, - selectedBlockLayerIds: getSelectedBlockLayerIds, - isActionMode, -}); - -export const getPropertyInputState = createStructuredSelector({ - animation: getAnimation, - isAnimationSelected, - selectedBlockIds: getSelectedBlockIds, - vectorLayer: getVectorLayer, - selectedLayerIds: getSelectedLayerIds, -}); - -export const getLayerListTreeState = createStructuredSelector({ - animation: getAnimation, - selectedLayerIds: getSelectedLayerIds, - collapsedLayerIds: getCollapsedLayerIds, - hiddenLayerIds: getHiddenLayerIds, - hoveredLayerId: getHoveredLayerId, - isActionMode, -}); - -export const getTimelineAnimationRowState = createStructuredSelector({ - animation: getAnimation, - collapsedLayerIds: getCollapsedLayerIds, - selectedBlockIds: getSelectedBlockIds, - isActionMode, -}); - -export const getLayerTimelineState = createStructuredSelector({ - animation: getAnimation, - vectorLayer: getVectorLayer, - isAnimationSelected, - selectedBlockIds: getSelectedBlockIds, - isBeingReset, - isActionMode, - actionMode: getActionMode, - singleSelectedPathBlock: getSingleSelectedPathBlock, -}); - -export const isWorkspaceDirty = createSelector( - [getVectorLayer, getAnimation], - (vl, anim) => vl.children.length > 0 || anim.blocks.length > 0, -); diff --git a/src/app/pages/editor/store/index.ts b/src/app/pages/editor/store/index.ts deleted file mode 100644 index 0a6a72df..00000000 --- a/src/app/pages/editor/store/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { StoreModule, Store, Action, ActionReducer } from '@ngrx/store'; -export { State, reducers, metaReducers } from './reducer'; diff --git a/src/app/pages/editor/store/layers/actions.ts b/src/app/pages/editor/store/layers/actions.ts deleted file mode 100644 index ecec81b1..00000000 --- a/src/app/pages/editor/store/layers/actions.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { VectorLayer } from 'app/pages/editor/model/layers'; -import { Action } from 'app/pages/editor/store'; - -export enum LayerActionTypes { - SetVectorLayer = '__layers__SET_VECTOR_LAYER', - SetSelectedLayers = '__layers__SET_SELECTED_LAYERS', - SetHiddenLayers = '__layers__SET_HIDDEN_LAYERS', - SetCollapsedLayers = '__layers__SET_COLLAPSED_LAYERS', -} - -export class SetVectorLayer implements Action { - readonly type = LayerActionTypes.SetVectorLayer; - readonly payload: { vectorLayer: VectorLayer }; - constructor(vectorLayer: VectorLayer) { - this.payload = { vectorLayer }; - } -} - -export class SetSelectedLayers implements Action { - readonly type = LayerActionTypes.SetSelectedLayers; - readonly payload: { layerIds: ReadonlySet }; - constructor(layerIds: ReadonlySet) { - this.payload = { layerIds }; - } -} - -export class SetHiddenLayers implements Action { - readonly type = LayerActionTypes.SetHiddenLayers; - readonly payload: { layerIds: ReadonlySet }; - constructor(layerIds: ReadonlySet) { - this.payload = { layerIds }; - } -} - -export class SetCollapsedLayers implements Action { - readonly type = LayerActionTypes.SetCollapsedLayers; - readonly payload: { layerIds: ReadonlySet }; - constructor(layerIds: ReadonlySet) { - this.payload = { layerIds }; - } -} - -export type LayerActions = - | SetVectorLayer - | SetSelectedLayers - | SetHiddenLayers - | SetCollapsedLayers; diff --git a/src/app/pages/editor/store/layers/reducer.ts b/src/app/pages/editor/store/layers/reducer.ts deleted file mode 100644 index 3302cab4..00000000 --- a/src/app/pages/editor/store/layers/reducer.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { VectorLayer } from 'app/pages/editor/model/layers'; - -import { LayerActionTypes, LayerActions } from './actions'; - -export interface State { - readonly vectorLayer: VectorLayer; - readonly selectedLayerIds: ReadonlySet; - readonly collapsedLayerIds: ReadonlySet; - readonly hiddenLayerIds: ReadonlySet; -} - -export function buildInitialState() { - return { - vectorLayer: new VectorLayer(), - selectedLayerIds: new Set(), - collapsedLayerIds: new Set(), - hiddenLayerIds: new Set(), - } as State; -} - -export function reducer(state = buildInitialState(), action: LayerActions) { - switch (action.type) { - case LayerActionTypes.SetVectorLayer: - return { ...state, vectorLayer: action.payload.vectorLayer }; - case LayerActionTypes.SetSelectedLayers: - return { ...state, selectedLayerIds: new Set(action.payload.layerIds) }; - case LayerActionTypes.SetHiddenLayers: - return { ...state, hiddenLayerIds: new Set(action.payload.layerIds) }; - case LayerActionTypes.SetCollapsedLayers: - return { ...state, collapsedLayerIds: new Set(action.payload.layerIds) }; - } - return state; -} diff --git a/src/app/pages/editor/store/layers/selectors.ts b/src/app/pages/editor/store/layers/selectors.ts deleted file mode 100644 index 600e8917..00000000 --- a/src/app/pages/editor/store/layers/selectors.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createDeepEqualSelector, getEditorState } from 'app/pages/editor/store/selectors'; -import { createSelector } from 'reselect'; - -const getLayerState = createSelector(getEditorState, s => s.layers); -export const getVectorLayer = createSelector(getLayerState, l => l.vectorLayer); -export const getSelectedLayerIds = createDeepEqualSelector(getLayerState, l => l.selectedLayerIds); -export const getCollapsedLayerIds = createDeepEqualSelector( - getLayerState, - l => l.collapsedLayerIds, -); -export const getHiddenLayerIds = createDeepEqualSelector(getLayerState, l => l.hiddenLayerIds); diff --git a/src/app/pages/editor/store/paper/actions.ts b/src/app/pages/editor/store/paper/actions.ts deleted file mode 100644 index 4dd4ceaa..00000000 --- a/src/app/pages/editor/store/paper/actions.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { CursorType, ToolMode } from 'app/pages/editor/model/paper'; -import { Point } from 'app/pages/editor/scripts/common'; -import { Action } from 'app/pages/editor/store'; - -export enum PaperActionTypes { - SetToolMode = '__paper__SET_TOOL_MODE', - SetSelectionBox = '__paper__SET_SELECTION_BOX', - SetCreatePathInfo = '__paper__SET_CREATE_PATH_INFO', - SetSplitCurveInfo = '__paper__SET_SPLIT_CURVE_INFO', - SetEditPathInfo = '__paper__SET_EDIT_PATH_INFO', - SetRotateItemsInfo = '__paper__SET_ROTATE_ITEMS_INFO', - SetTransformPathInfo = '__paper__SET_TRANSFORM_PATHS_INFO', - SetCursorType = '__paper__SET_CANVAS_CURSOR', - SetSnapGuideInfo = '__paper__SET_SNAP_GUIDE_INFO', - SetZoomPanInfo = '__paper__SET_ZOOM_PAN_INFO', - SetTooltipInfo = '__paper__SET_TOOLTIP_INFO', - SetHoveredLayerId = '__paper__SET_HOVERED_LAYER_ID', -} - -export class SetToolMode implements Action { - readonly type = PaperActionTypes.SetToolMode; - constructor(readonly toolMode: ToolMode) {} -} - -export class SetSelectionBox implements Action { - readonly type = PaperActionTypes.SetSelectionBox; - constructor(readonly selectionBox: SelectionBox | undefined) {} -} - -export class SetCreatePathInfo implements Action { - readonly type = PaperActionTypes.SetCreatePathInfo; - constructor(readonly createPathInfo: CreatePathInfo | undefined) {} -} - -export class SetSplitCurveInfo implements Action { - readonly type = PaperActionTypes.SetSplitCurveInfo; - constructor(readonly splitCurveInfo: SplitCurveInfo | undefined) {} -} - -export class SetEditPathInfo implements Action { - readonly type = PaperActionTypes.SetEditPathInfo; - constructor(readonly editPathInfo: EditPathInfo | undefined) {} -} - -export class SetRotateItemsInfo implements Action { - readonly type = PaperActionTypes.SetRotateItemsInfo; - constructor(readonly rotateItemsInfo: RotateItemsInfo | undefined) {} -} - -export class SetTransformPathsInfo implements Action { - readonly type = PaperActionTypes.SetTransformPathInfo; - constructor(readonly transformPathsInfo: TransformPathsInfo | undefined) {} -} - -export class SetCursorType implements Action { - readonly type = PaperActionTypes.SetCursorType; - constructor(readonly cursorType: CursorType) {} -} - -export class SetSnapGuideInfo implements Action { - readonly type = PaperActionTypes.SetSnapGuideInfo; - constructor(readonly snapGuideInfo: SnapGuideInfo | undefined) {} -} - -export class SetZoomPanInfo implements Action { - readonly type = PaperActionTypes.SetZoomPanInfo; - constructor(readonly zoomPanInfo: ZoomPanInfo) {} -} - -export class SetTooltipInfo implements Action { - readonly type = PaperActionTypes.SetTooltipInfo; - constructor(readonly tooltipInfo: TooltipInfo | undefined) {} -} - -export class SetHoveredLayerId implements Action { - readonly type = PaperActionTypes.SetHoveredLayerId; - constructor(readonly hoveredLayerId: string | undefined) {} -} - -export type PaperActions = - | SetToolMode - | SetSelectionBox - | SetCreatePathInfo - | SetSplitCurveInfo - | SetEditPathInfo - | SetRotateItemsInfo - | SetTransformPathsInfo - | SetCursorType - | SetSnapGuideInfo - | SetZoomPanInfo - | SetTooltipInfo - | SetHoveredLayerId; - -export interface SelectionBox { - readonly from: Point; - readonly to: Point; -} - -export interface CreatePathInfo { - readonly pathData: string; - readonly strokeColor: string; -} - -export type Segment = Readonly<{ point: Point; handleIn: Point; handleOut: Point }>; - -export interface SplitCurveInfo { - readonly splitPoint: Point; - readonly segment1: Segment; - readonly segment2: Segment; -} - -export interface EditPathInfo { - // TODO: suffix these variables with 'index' - readonly selectedSegments: ReadonlySet; - readonly visibleHandleIns: ReadonlySet; - readonly visibleHandleOuts: ReadonlySet; - readonly selectedHandleIn: number | undefined; - readonly selectedHandleOut: number | undefined; -} - -export interface RotateItemsInfo { - // If a pivot isn't provided, then the center of the selected items' bounding - // box will be used instead. - readonly pivot?: Point | undefined; -} - -export interface TransformPathsInfo { - // TODO: add selected segment info? (similar to sketch) -} - -export type Line = Readonly<{ from: Point; to: Point }>; - -export interface SnapGuideInfo { - readonly guides: ReadonlyArray; - readonly rulers: ReadonlyArray; -} - -export interface ZoomPanInfo { - readonly zoom: number; - readonly translation: Readonly<{ tx: number; ty: number }>; -} - -export interface TooltipInfo { - readonly point: Point; - readonly label: string; -} diff --git a/src/app/pages/editor/store/paper/reducer.ts b/src/app/pages/editor/store/paper/reducer.ts deleted file mode 100644 index 77280da3..00000000 --- a/src/app/pages/editor/store/paper/reducer.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { CursorType, ToolMode } from 'app/pages/editor/model/paper'; - -import { - CreatePathInfo, - EditPathInfo, - PaperActionTypes, - PaperActions, - RotateItemsInfo, - SelectionBox, - SnapGuideInfo, - SplitCurveInfo, - TooltipInfo, - TransformPathsInfo, - ZoomPanInfo, -} from './actions'; - -// Note that we should only ever store points in their viewport/local coordinates. -// We should never store points in coordinates that are dependent on the view. - -export interface State { - readonly zoomPanInfo: ZoomPanInfo; - readonly toolModeInfo: ToolModeInfo; - readonly cursorType: CursorType; -} - -interface ToolModeInfo { - readonly toolMode: ToolMode; - readonly selectionBox?: SelectionBox; - readonly createPathInfo?: CreatePathInfo; - readonly splitCurveInfo?: SplitCurveInfo; - readonly editPathInfo?: EditPathInfo; - readonly rotateItemsInfo?: RotateItemsInfo; - readonly transformPathsInfo?: TransformPathsInfo; - readonly snapGuideInfo?: SnapGuideInfo; - readonly tooltipInfo?: TooltipInfo; - readonly hoveredLayerId?: string; -} - -export function buildInitialState(): State { - return { - zoomPanInfo: { zoom: 1, translation: { tx: 0, ty: 0 } }, - toolModeInfo: { toolMode: ToolMode.Default }, - cursorType: CursorType.Default, - }; -} - -export function reducer(state = buildInitialState(), action: PaperActions): State { - const { toolModeInfo } = state; - switch (action.type) { - case PaperActionTypes.SetZoomPanInfo: - return { ...state, zoomPanInfo: action.zoomPanInfo }; - case PaperActionTypes.SetToolMode: - // TODO: don't wipe out all of the other tool mode info here... - return { ...state, toolModeInfo: { toolMode: action.toolMode } }; - case PaperActionTypes.SetSelectionBox: - return { ...state, toolModeInfo: { ...toolModeInfo, selectionBox: action.selectionBox } }; - case PaperActionTypes.SetCreatePathInfo: - return { ...state, toolModeInfo: { ...toolModeInfo, createPathInfo: action.createPathInfo } }; - case PaperActionTypes.SetSplitCurveInfo: - return { ...state, toolModeInfo: { ...toolModeInfo, splitCurveInfo: action.splitCurveInfo } }; - case PaperActionTypes.SetEditPathInfo: - const { editPathInfo } = action; - return { ...state, toolModeInfo: { ...toolModeInfo, editPathInfo } }; - case PaperActionTypes.SetRotateItemsInfo: - const { rotateItemsInfo } = action; - return { ...state, toolModeInfo: { ...toolModeInfo, rotateItemsInfo } }; - case PaperActionTypes.SetTransformPathInfo: - const { transformPathsInfo } = action; - return { ...state, toolModeInfo: { ...toolModeInfo, transformPathsInfo } }; - case PaperActionTypes.SetSnapGuideInfo: - return { ...state, toolModeInfo: { ...toolModeInfo, snapGuideInfo: action.snapGuideInfo } }; - case PaperActionTypes.SetTooltipInfo: - return { ...state, toolModeInfo: { ...toolModeInfo, tooltipInfo: action.tooltipInfo } }; - case PaperActionTypes.SetCursorType: - return { ...state, cursorType: action.cursorType }; - case PaperActionTypes.SetHoveredLayerId: - const { hoveredLayerId } = action; - return { ...state, toolModeInfo: { ...toolModeInfo, hoveredLayerId } }; - } - return state; -} diff --git a/src/app/pages/editor/store/paper/selectors.ts b/src/app/pages/editor/store/paper/selectors.ts deleted file mode 100644 index 9696f786..00000000 --- a/src/app/pages/editor/store/paper/selectors.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { ToolMode } from 'app/pages/editor/model/paper'; -import { getSelectedLayerIds, getVectorLayer } from 'app/pages/editor/store/layers/selectors'; -import { createDeepEqualSelector, getEditorState } from 'app/pages/editor/store/selectors'; -import { createSelector, createStructuredSelector } from 'reselect'; - -const getPaperState = createSelector(getEditorState, s => s.paper); -export const getZoomPanInfo = createDeepEqualSelector(getPaperState, p => p.zoomPanInfo); -const getToolModeInfo = createSelector(getPaperState, p => p.toolModeInfo); -export const getToolMode = createDeepEqualSelector(getToolModeInfo, p => p.toolMode); -export const getSelectionBox = createDeepEqualSelector(getToolModeInfo, p => p.selectionBox); -export const getCreatePathInfo = createDeepEqualSelector(getToolModeInfo, p => p.createPathInfo); -export const getSplitCurveInfo = createDeepEqualSelector(getToolModeInfo, p => p.splitCurveInfo); -export const getEditPathInfo = createDeepEqualSelector(getToolModeInfo, p => p.editPathInfo); -export const getRotateItemsInfo = createDeepEqualSelector(getToolModeInfo, p => p.rotateItemsInfo); -export const getTransformPathsInfo = createDeepEqualSelector( - getToolModeInfo, - p => p.transformPathsInfo, -); -export const getSnapGuideInfo = createDeepEqualSelector(getToolModeInfo, p => p.snapGuideInfo); -export const getTooltipInfo = createDeepEqualSelector(getToolModeInfo, p => p.tooltipInfo); -export const getCursorType = createDeepEqualSelector(getPaperState, p => p.cursorType); -export const getHoveredLayerId = createDeepEqualSelector(getToolModeInfo, p => p.hoveredLayerId); - -const getSingleSelectedChildlessLayer = createSelector( - [getVectorLayer, getSelectedLayerIds], - (vl, layerIds) => { - if (layerIds.size !== 1) { - return undefined; - } - const layerId = layerIds.values().next().value; - const layer = vl.findLayerById(layerId); - // TODO: consolidate this logic in a single place (the layer.children.length check is used in gestures too) - return layer.children.length ? undefined : layer; - }, -); - -const isEditPathChecked = createSelector(getEditPathInfo, epi => !!epi); -// TODO: exclude empty groups for rotate items? -const isRotateItemsEnabled = createSelector(getSelectedLayerIds, layerIds => layerIds.size > 0); -const isRotateItemsChecked = createSelector(getRotateItemsInfo, rii => !!rii); -const isTransformPathsEnabled = createSelector(getSingleSelectedChildlessLayer, layer => !!layer); -const isTransformPathsChecked = createSelector(getTransformPathsInfo, tpi => !!tpi); -const isDefaultChecked = createSelector( - [getToolMode, isEditPathChecked, isRotateItemsChecked, isTransformPathsChecked], - ( - toolMode: ToolMode, - editPathChecked: boolean, - rotateItemsChecked: boolean, - transformPathChecked: boolean, - ) => { - return ( - toolMode === ToolMode.Default && - !editPathChecked && - !rotateItemsChecked && - !transformPathChecked - ); - }, -); - -export const getToolPanelState = createStructuredSelector({ - toolMode: getToolMode, - isDefaultChecked, - isEditPathChecked, - isRotateItemsEnabled, - isRotateItemsChecked, - isTransformPathsEnabled, - isTransformPathsChecked, -}); diff --git a/src/app/pages/editor/store/playback/actions.ts b/src/app/pages/editor/store/playback/actions.ts deleted file mode 100644 index 96b8ac07..00000000 --- a/src/app/pages/editor/store/playback/actions.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Action } from 'app/pages/editor/store'; - -export enum PlaybackActionTypes { - SetIsSlowMotion = '__playback__SET_IS_SLOW_MOTION', - SetIsPlaying = '__playback__SET_IS_PLAYING', - SetIsRepeating = '__playback__SET_IS_REPEATING', - SetCurrentTime = '__playback__SET_CURRENT_TIME', -} - -export class SetIsSlowMotion implements Action { - readonly type = PlaybackActionTypes.SetIsSlowMotion; - readonly payload: { isSlowMotion: boolean }; - constructor(isSlowMotion: boolean) { - this.payload = { isSlowMotion }; - } -} - -export class SetIsPlaying implements Action { - readonly type = PlaybackActionTypes.SetIsPlaying; - readonly payload: { isPlaying: boolean }; - constructor(isPlaying: boolean) { - this.payload = { isPlaying }; - } -} - -export class SetIsRepeating implements Action { - readonly type = PlaybackActionTypes.SetIsRepeating; - readonly payload: { isRepeating: boolean }; - constructor(isRepeating: boolean) { - this.payload = { isRepeating }; - } -} - -export class SetCurrentTime implements Action { - readonly type = PlaybackActionTypes.SetCurrentTime; - readonly payload: { currentTime: number }; - constructor(currentTime: number) { - this.payload = { currentTime }; - } -} - -export type PlaybackActions = SetIsSlowMotion | SetIsPlaying | SetIsRepeating | SetCurrentTime; diff --git a/src/app/pages/editor/store/playback/reducer.ts b/src/app/pages/editor/store/playback/reducer.ts deleted file mode 100644 index b06a0932..00000000 --- a/src/app/pages/editor/store/playback/reducer.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { PlaybackActionTypes, PlaybackActions } from './actions'; - -export interface State { - readonly isSlowMotion: boolean; - readonly isPlaying: boolean; - readonly isRepeating: boolean; - readonly currentTime: number; -} - -export function buildInitialState() { - return { - isSlowMotion: false, - isPlaying: false, - isRepeating: false, - currentTime: 0, - } as State; -} - -export function reducer(state = buildInitialState(), action: PlaybackActions) { - switch (action.type) { - case PlaybackActionTypes.SetIsSlowMotion: - return { ...state, isSlowMotion: action.payload.isSlowMotion }; - case PlaybackActionTypes.SetIsPlaying: - return { ...state, isPlaying: action.payload.isPlaying }; - case PlaybackActionTypes.SetIsRepeating: - return { ...state, isRepeating: action.payload.isRepeating }; - case PlaybackActionTypes.SetCurrentTime: - return { ...state, currentTime: action.payload.currentTime }; - } - return state; -} diff --git a/src/app/pages/editor/store/playback/selectors.ts b/src/app/pages/editor/store/playback/selectors.ts deleted file mode 100644 index 8cfe7e8b..00000000 --- a/src/app/pages/editor/store/playback/selectors.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { AnimationRenderer } from 'app/pages/editor/scripts/animator'; -import { getVectorLayer } from 'app/pages/editor/store/layers/selectors'; -import { getEditorState } from 'app/pages/editor/store/selectors'; -import { getAnimation } from 'app/pages/editor/store/timeline/selectors'; -import { createSelector } from 'reselect'; - -export const getPlaybackState = createSelector(getEditorState, s => s.playback); -export const getIsSlowMotion = createSelector(getPlaybackState, p => p.isSlowMotion); -export const getIsPlaying = createSelector(getPlaybackState, p => p.isPlaying); -export const getIsRepeating = createSelector(getPlaybackState, p => p.isRepeating); -export const getCurrentTime = createSelector(getPlaybackState, p => p.currentTime); - -const getAnimationRenderer = createSelector( - [getVectorLayer, getAnimation], - (vl, anim) => new AnimationRenderer(vl, anim), -); - -export const getAnimatedVectorLayer = createSelector( - [getAnimationRenderer, getCurrentTime], - (animationRenderer, currentTime) => { - const vl = animationRenderer.setCurrentTime(currentTime); - return { vl, currentTime }; - }, -); diff --git a/src/app/pages/editor/store/reducer.ts b/src/app/pages/editor/store/reducer.ts deleted file mode 100644 index 7c96ebb5..00000000 --- a/src/app/pages/editor/store/reducer.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { environment } from 'environments/environment'; -import { storeLogger } from 'ngrx-store-logger'; - -import * as fromActionMode from './actionmode/reducer'; -import * as metaBatchAction from './batch/metareducer'; -import * as fromLayers from './layers/reducer'; -import * as fromPaper from './paper/reducer'; -import * as fromPlayback from './playback/reducer'; -import * as metaReset from './reset/metareducer'; -import * as fromReset from './reset/reducer'; -import * as metaStoreFreeze from './storefreeze/metareducer'; -import * as fromTheme from './theme/reducer'; -import * as fromTimeline from './timeline/reducer'; -import * as metaUndoRedo from './undoredo/metareducer'; - -export type State = metaUndoRedo.StateWithHistoryAndTimestamp; - -export interface EditorState { - readonly layers: fromLayers.State; - readonly timeline: fromTimeline.State; - readonly playback: fromPlayback.State; - readonly actionmode: fromActionMode.State; - readonly reset: fromReset.State; - readonly theme: fromTheme.State; - readonly paper: fromPaper.State; -} - -export const reducers = { - layers: fromLayers.reducer, - timeline: fromTimeline.reducer, - playback: fromPlayback.reducer, - actionmode: fromActionMode.reducer, - reset: fromReset.reducer, - theme: fromTheme.reducer, - paper: fromPaper.reducer, -}; - -const prodMetaReducers = [ - // Meta-reducer that records past/present/future state. - metaUndoRedo.metaReducer, - // Meta-reducer that adds the ability to dispatch multiple actions at a time. - metaBatchAction.metaReducer, - // Meta-reducer that adds the ability to reset the entire state tree. - metaReset.metaReducer, -]; - -const devMetaReducers = [ - // Meta reducer that logs the before/after state of the store - // as actions are performed in dev builds. - storeLogger({ collapsed: true }), - // Meta reducer that freezes the state tree to ensure that - // accidental mutations fail fast in dev builds. - metaStoreFreeze.metaReducer, -]; - -export const metaReducers = environment.production - ? prodMetaReducers - : [...devMetaReducers, ...prodMetaReducers]; diff --git a/src/app/pages/editor/store/reset/actions.ts b/src/app/pages/editor/store/reset/actions.ts deleted file mode 100644 index 915212f6..00000000 --- a/src/app/pages/editor/store/reset/actions.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { VectorLayer } from 'app/pages/editor/model/layers'; -import { Animation } from 'app/pages/editor/model/timeline'; -import { Action } from 'app/pages/editor/store'; - -export enum ResetActionTypes { - ResetWorkspace = '__reset__RESET_WORKSPACE', -} - -export class ResetWorkspace implements Action { - readonly type = ResetActionTypes.ResetWorkspace; - readonly payload: { - vectorLayer?: VectorLayer; - animation?: Animation; - hiddenLayerIds?: ReadonlySet; - }; - constructor( - vectorLayer?: VectorLayer, - animation?: Animation, - hiddenLayerIds?: ReadonlySet, - ) { - this.payload = { vectorLayer, animation, hiddenLayerIds }; - } -} - -export type ResetActions = ResetWorkspace; diff --git a/src/app/pages/editor/store/reset/metareducer.ts b/src/app/pages/editor/store/reset/metareducer.ts deleted file mode 100644 index 3b05989c..00000000 --- a/src/app/pages/editor/store/reset/metareducer.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ActionReducer } from 'app/pages/editor/store'; -import { EditorState } from 'app/pages/editor/store/reducer'; - -import { ResetActionTypes, ResetActions } from './actions'; - -export function metaReducer(reducer: ActionReducer): ActionReducer { - return (state: EditorState, action: ResetActions) => { - if (action.type === ResetActionTypes.ResetWorkspace) { - state = undefined; - } - state = reducer(state, action); - if (action.type === ResetActionTypes.ResetWorkspace) { - const { vectorLayer, animation, hiddenLayerIds } = action.payload; - if (vectorLayer) { - const { layers } = state; - state = { - ...state, - layers: { - ...layers, - vectorLayer, - hiddenLayerIds, - }, - }; - } - if (animation) { - const { timeline } = state; - state = { - ...state, - timeline: { - ...timeline, - animation, - }, - }; - } - } - return state; - }; -} diff --git a/src/app/pages/editor/store/reset/reducer.ts b/src/app/pages/editor/store/reset/reducer.ts deleted file mode 100644 index 5a3a3f2a..00000000 --- a/src/app/pages/editor/store/reset/reducer.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ResetActionTypes, ResetActions } from './actions'; - -// TODO: remove this 'isBeingReset' flag... see TODO in layer timeline component -export interface State { - readonly isBeingReset: boolean; -} - -export function buildInitialState() { - return { - isBeingReset: false, - } as State; -} - -export function reducer(state = buildInitialState(), action: ResetActions) { - if (action.type === ResetActionTypes.ResetWorkspace) { - return { ...state, isBeingReset: true }; - } - const { isBeingReset } = state; - if (isBeingReset) { - return { ...state, isBeingReset: false }; - } - return state; -} diff --git a/src/app/pages/editor/store/reset/selectors.ts b/src/app/pages/editor/store/reset/selectors.ts deleted file mode 100644 index b7af6ff7..00000000 --- a/src/app/pages/editor/store/reset/selectors.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { getEditorState } from 'app/pages/editor/store/selectors'; -import { createSelector } from 'reselect'; - -const getResetState = createSelector(getEditorState, s => s.reset); -export const isBeingReset = createSelector(getResetState, r => r.isBeingReset); diff --git a/src/app/pages/editor/store/selectors.ts b/src/app/pages/editor/store/selectors.ts deleted file mode 100644 index f7a922c0..00000000 --- a/src/app/pages/editor/store/selectors.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as _ from 'lodash'; -import { createSelector, createSelectorCreator, defaultMemoize } from 'reselect'; - -import { State } from './reducer'; - -const getState = (state: State) => state; -export const getEditorState = createSelector(getState, s => s.present); -export const createDeepEqualSelector = createSelectorCreator(defaultMemoize, _.isEqual); diff --git a/src/app/pages/editor/store/storefreeze/metareducer.ts b/src/app/pages/editor/store/storefreeze/metareducer.ts deleted file mode 100644 index 6dedae99..00000000 --- a/src/app/pages/editor/store/storefreeze/metareducer.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Action, ActionReducer } from 'app/pages/editor/store'; -import * as deepFreeze from 'deep-freeze-strict'; - -/** - * Meta reducer that prevents state from being mutated anywhere in the app. - */ -export function metaReducer(reducer: ActionReducer): ActionReducer { - return (state: T, action: Action) => { - if (state) { - deepFreeze(state); - } - const nextState = reducer(state, action); - if (nextState) { - deepFreeze(nextState); - } - return nextState; - }; -} diff --git a/src/app/pages/editor/store/theme/actions.ts b/src/app/pages/editor/store/theme/actions.ts deleted file mode 100644 index 70796f14..00000000 --- a/src/app/pages/editor/store/theme/actions.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Action } from 'app/pages/editor/store'; - -import { ThemeType } from './reducer'; - -export enum ThemeActionTypes { - SetTheme = '__theme__SET_THEME', -} - -export class SetTheme implements Action { - readonly type = ThemeActionTypes.SetTheme; - readonly payload: { themeType: ThemeType }; - constructor(themeType: ThemeType) { - this.payload = { themeType }; - } -} - -export type ThemeActions = SetTheme; diff --git a/src/app/pages/editor/store/theme/reducer.ts b/src/app/pages/editor/store/theme/reducer.ts deleted file mode 100644 index b77c3481..00000000 --- a/src/app/pages/editor/store/theme/reducer.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ThemeActionTypes, ThemeActions } from './actions'; - -const STORAGE_KEY_THEME_TYPE = 'storage_key_theme_type'; -export type ThemeType = 'light' | 'dark'; - -export interface State { - readonly themeType: ThemeType; - readonly isInitialPageLoad: boolean; -} - -export function buildInitialState() { - return { - themeType: window.localStorage.getItem(STORAGE_KEY_THEME_TYPE) || 'light', - isInitialPageLoad: true, - } as State; -} - -export function reducer(state = buildInitialState(), action: ThemeActions) { - if (action.type === ThemeActionTypes.SetTheme) { - const { themeType } = action.payload; - window.localStorage.setItem(STORAGE_KEY_THEME_TYPE, themeType); - if (themeType === state.themeType) { - return state; - } - return { ...state, themeType, isInitialPageLoad: false }; - } - return state; -} diff --git a/src/app/pages/editor/store/theme/selectors.ts b/src/app/pages/editor/store/theme/selectors.ts deleted file mode 100644 index 034a4e83..00000000 --- a/src/app/pages/editor/store/theme/selectors.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { getEditorState } from 'app/pages/editor/store/selectors'; -import { createSelector, createStructuredSelector } from 'reselect'; - -const getThemeState = createSelector(getEditorState, s => s.theme); -export const getThemeType = createStructuredSelector({ - themeType: createSelector(getThemeState, t => t.themeType), - isInitialPageLoad: createSelector(getThemeState, t => t.isInitialPageLoad), -}); diff --git a/src/app/pages/editor/store/timeline/actions.ts b/src/app/pages/editor/store/timeline/actions.ts deleted file mode 100644 index 924bd750..00000000 --- a/src/app/pages/editor/store/timeline/actions.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Animation } from 'app/pages/editor/model/timeline'; -import { Action } from 'app/pages/editor/store'; - -export enum TimelineActionTypes { - SetAnimation = '__timeline__SET_ANIMATION', - SelectAnimation = '__timeline__SELECT_ANIMATION', - SetSelectedBlocks = '__timeline__SET_SELECTED_BLOCKS', -} - -export class SetAnimation implements Action { - readonly type = TimelineActionTypes.SetAnimation; - readonly payload: { animation: Animation }; - constructor(animation: Animation) { - this.payload = { animation }; - } -} - -export class SelectAnimation implements Action { - readonly type = TimelineActionTypes.SelectAnimation; - readonly payload: { isAnimationSelected: boolean }; - constructor(isAnimationSelected: boolean) { - this.payload = { isAnimationSelected }; - } -} - -export class SetSelectedBlocks implements Action { - readonly type = TimelineActionTypes.SetSelectedBlocks; - readonly payload: { blockIds: ReadonlySet }; - constructor(blockIds: ReadonlySet) { - this.payload = { blockIds }; - } -} - -export type TimelineActions = SetAnimation | SelectAnimation | SetSelectedBlocks; diff --git a/src/app/pages/editor/store/timeline/reducer.ts b/src/app/pages/editor/store/timeline/reducer.ts deleted file mode 100644 index 9e6dbcdd..00000000 --- a/src/app/pages/editor/store/timeline/reducer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Animation } from 'app/pages/editor/model/timeline'; - -import { TimelineActionTypes, TimelineActions } from './actions'; - -export interface State { - readonly animation: Animation; - readonly isAnimationSelected: boolean; - readonly selectedBlockIds: ReadonlySet; -} - -export function buildInitialState() { - return { - animation: new Animation(), - isAnimationSelected: false, - selectedBlockIds: new Set(), - } as State; -} - -export function reducer(state = buildInitialState(), action: TimelineActions) { - switch (action.type) { - case TimelineActionTypes.SetAnimation: - return { ...state, animation: action.payload.animation }; - case TimelineActionTypes.SelectAnimation: - return { ...state, isAnimationSelected: action.payload.isAnimationSelected }; - case TimelineActionTypes.SetSelectedBlocks: - return { ...state, selectedBlockIds: new Set(action.payload.blockIds) }; - } - return state; -} diff --git a/src/app/pages/editor/store/timeline/selectors.ts b/src/app/pages/editor/store/timeline/selectors.ts deleted file mode 100644 index 42413f15..00000000 --- a/src/app/pages/editor/store/timeline/selectors.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { PathAnimationBlock } from 'app/pages/editor/model/timeline'; -import { createDeepEqualSelector, getEditorState } from 'app/pages/editor/store/selectors'; -import * as _ from 'lodash'; -import { createSelector } from 'reselect'; - -const getTimelineState = createSelector(getEditorState, s => s.timeline); -export const getAnimation = createSelector(getTimelineState, t => t.animation); -export const isAnimationSelected = createSelector(getTimelineState, t => t.isAnimationSelected); -export const getSelectedBlockIds = createDeepEqualSelector( - getTimelineState, - t => t.selectedBlockIds, -); -export const getSingleSelectedBlockId = createSelector( - getSelectedBlockIds, - blockIds => (blockIds.size === 1 ? blockIds.values().next().value : undefined), -); -export const getSingleSelectedPathBlock = createSelector( - [getAnimation, getSingleSelectedBlockId], - (anim, blockId) => { - if (!blockId) { - return undefined; - } - return _.find( - anim.blocks, - b => b.id === blockId && b instanceof PathAnimationBlock, - ) as PathAnimationBlock; - }, -); -export const getSelectedBlockLayerIds = createDeepEqualSelector( - [getAnimation, getSelectedBlockIds], - (anim, blockIds) => { - return new Set(Array.from(blockIds).map(id => _.find(anim.blocks, b => b.id === id).layerId)); - }, -); -export const getSingleSelectedBlockLayerId = createSelector( - getSelectedBlockLayerIds, - blockLayerIds => (blockLayerIds.size === 1 ? blockLayerIds.values().next().value : undefined), -); diff --git a/src/app/pages/editor/store/undoredo/metareducer.ts b/src/app/pages/editor/store/undoredo/metareducer.ts deleted file mode 100644 index 06c9efbb..00000000 --- a/src/app/pages/editor/store/undoredo/metareducer.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Action, ActionReducer } from 'app/pages/editor/store'; -import { ActionModeActionTypes } from 'app/pages/editor/store/actionmode/actions'; -import { PlaybackActionTypes } from 'app/pages/editor/store/playback/actions'; -import { EditorState } from 'app/pages/editor/store/reducer'; -import undoable, { StateWithHistory, UndoableOptions, excludeAction } from 'redux-undo'; - -const UNDO_HISTORY_SIZE = 30; -const UNDO_DEBOUNCE_MILLIS = 1000; -const UNDO_EXCLUDED_ACTIONS = [ - PlaybackActionTypes.SetIsSlowMotion, - PlaybackActionTypes.SetIsPlaying, - PlaybackActionTypes.SetIsRepeating, - PlaybackActionTypes.SetCurrentTime, - ActionModeActionTypes.SetActionMode, - ActionModeActionTypes.SetActionModeHover, -]; - -let groupCounter = 1; - -export interface StateWithHistoryAndTimestamp extends StateWithHistory { - timestamp: number; -} - -type StateReducer = ActionReducer; -type EditorStateReducer = ActionReducer; - -export function metaReducer(reducer: EditorStateReducer): StateReducer { - const undoableReducer = undoable(reducer, { - limit: UNDO_HISTORY_SIZE, - filter: excludeAction(UNDO_EXCLUDED_ACTIONS), - groupBy: (action: Action, currState: EditorState, prevState: StateWithHistoryAndTimestamp) => { - if (Date.now() - prevState.timestamp < UNDO_DEBOUNCE_MILLIS) { - return groupCounter; - } - groupCounter++; - return undefined; - }, - } as UndoableOptions); - return (state: StateWithHistoryAndTimestamp, action: Action) => { - return { ...undoableReducer(state, action), timestamp: Date.now() }; - }; -} diff --git a/src/app/pages/editor/styles/app.scss b/src/app/pages/editor/styles/app.scss deleted file mode 100644 index 2c7d567f..00000000 --- a/src/app/pages/editor/styles/app.scss +++ /dev/null @@ -1,4 +0,0 @@ -// Core page layouts and common stuff. -@import 'root'; -// Material design theme. -@import 'theme'; diff --git a/src/app/pages/editor/styles/material-icons.scss b/src/app/pages/editor/styles/material-icons.scss deleted file mode 100644 index 7507da7d..00000000 --- a/src/app/pages/editor/styles/material-icons.scss +++ /dev/null @@ -1,28 +0,0 @@ -@mixin use-icon-font { - font-weight: normal; - font-style: normal; - font-size: 24px; // Preferred icon size - display: inline-block; - width: 1em; - height: 1em; - line-height: 1; - text-transform: none; - letter-spacing: normal; - word-wrap: normal; // Support for all WebKit browsers. - -webkit-font-smoothing: antialiased; // Support for Safari and Chrome. - text-rendering: optimizeLegibility; // Support for Firefox. - -moz-osx-font-smoothing: grayscale; // Support for IE. - -webkit-font-feature-settings: 'liga'; - -moz-font-feature-settings: 'liga'; - font-feature-settings: 'liga'; // Custom added for GMP - user-select: none; -} - -@mixin material-icons { - @include use-icon-font; - font-family: 'Material Icons'; -} - -.material-icons { - @include material-icons; -} diff --git a/src/app/pages/editor/styles/root.scss b/src/app/pages/editor/styles/root.scss deleted file mode 100644 index 16ba40bb..00000000 --- a/src/app/pages/editor/styles/root.scss +++ /dev/null @@ -1,15 +0,0 @@ -html, -body { - margin: 0; - padding: 0; - width: 100%; - height: 100%; - overflow: hidden; -} - -$mdFontFamily: Roboto, -'Helvetica Neue', -sans-serif !default; -html { - font-family: $mdFontFamily; -} diff --git a/src/app/pages/editor/styles/theme.scss b/src/app/pages/editor/styles/theme.scss deleted file mode 100644 index bf5189ee..00000000 --- a/src/app/pages/editor/styles/theme.scss +++ /dev/null @@ -1,165 +0,0 @@ -@import '~@angular/material/theming'; -@import '../components/canvas/_canvas-theme.scss'; -@import '../components/dialogs/_dialog-theme.scss'; -@import '../components/layertimeline/_layerlisttree-theme.scss'; -@import '../components/layertimeline/_layertimeline-theme.scss'; -@import '../components/layertimeline/_timelineanimationrow-theme.scss'; -@import '../components/playback/_playback-theme.scss'; -@import '../components/propertyinput/_propertyinput-theme.scss'; -@import '../components/root/_root-theme.scss'; -@import '../components/toolbar/_toolbar-theme.scss'; -@include mat-core(); - -$ss-light-theme-foreground: ( - divider: rgba(black, 0.12), - divider-inverse: rgba(white, 0.12), - disabled-text: rgba(black, 0.38), - disabled-text-inverse: rgba(white, 0.5), - secondary-text: rgba(black, 0.54), - secondary-text-inverse: rgba(white, 0.7), - primary-text: rgba(black, 0.87), - primary-text-inverse: rgba(white, 1), -); - -$ss-dark-theme-foreground: ( - divider: rgba(white, 0.12), - divider-inverse: rgba(black, 0.12), - disabled-text: rgba(white, 0.5), - disabled-text-inverse: rgba(black, 0.38), - secondary-text: rgba(white, 0.7), - secondary-text-inverse: rgba(black, 0.54), - primary-text: rgba(white, 1), - primary-text-inverse: rgba(black, 0.87), -); - -$ss-light-theme-background: ( - base: white, - base50: map-get($mat-grey, 50), - base100: map-get($mat-grey, 100), - base200: map-get($mat-grey, 200), - base200-inverse: map-get($mat-grey, 900), - base300: map-get($mat-grey, 300), -); - -$ss-dark-theme-background: ( - base: black, - base50: #303030, - base100: map_get($mat-grey, 800), - base200: map_get($mat-grey, 900), - base200-inverse: map-get($mat-grey, 200), - base300: #101010, -); - -$light-accent: mat-palette($mat-blue, A400, A200, A700); -$dark-accent: mat-palette($mat-deep-orange, A200, A100, A400); - -@function ss-light-theme($primary, $accent, $warn: mat-palette($mat-red)) { - $base-theme: mat-light-theme($primary, $accent, $warn); - $result: map_merge($base-theme, ( - accent-inverse: $dark-accent, - ss-foreground: $ss-light-theme-foreground, - ss-background: $ss-light-theme-background, - )); - @return $result; -} - -@function ss-dark-theme($primary, $accent, $warn: mat-palette($mat-red)) { - $base-theme: mat-dark-theme($primary, $accent, $warn); - $result: map_merge($base-theme, ( - accent-inverse: $light-accent, - ss-foreground: $ss-dark-theme-foreground, - ss-background: $ss-dark-theme-background, - )); - @return $result; -} - -@mixin build-custom-theme($theme) { - @include angular-material-theme($theme); - @include ss-canvas-theme($theme); - @include ss-dialog-theme($theme); - @include ss-layerlisttree-theme($theme); - @include ss-layertimeline-theme($theme); - @include ss-timelineanimationrow-theme($theme); - @include ss-playback-theme($theme); - @include ss-propertyinput-theme($theme); - @include ss-root-theme($theme); - @include ss-toolbar-theme($theme); -} - -$light-primary: mat-palette($mat-blue-grey, 500); -$light-warn: mat-palette($mat-red); -$dark-primary: mat-palette($mat-indigo, 700); -$dark-warn: mat-palette($mat-red); -$light-theme: ss-light-theme($light-primary, $light-accent, $light-warn); -$dark-theme: ss-dark-theme($dark-primary, $dark-accent, $dark-warn); -@include build-custom-theme($light-theme); -.ss-dark-theme { - @include build-custom-theme($dark-theme); -} - -@mixin theme-animation-properties { - animation-duration: 200ms; - animation-timing-function: ease-out; - animation-fill-mode: forwards; -} - -@mixin build-theme-animations($themeKey, $paletteKey, $animName) { - $light: map-get($light-theme, $themeKey); - $dark: map-get($dark-theme, $themeKey); - .dark-to-light-#{$animName} { - animation-name: darkToLight#{$animName}; - @include theme-animation-properties; - } - .light-to-dark-#{$animName} { - animation-name: lightToDark#{$animName}; - @include theme-animation-properties; - } - @keyframes lightToDark#{$animName} { - 0% { background-color: mat-color($light, $paletteKey); } - 100% { background-color: mat-color($dark, $paletteKey); } - } - @keyframes darkToLight#{$animName} { - 0% { background-color: mat-color($dark, $paletteKey); } - 100% { background-color: mat-color($light, $paletteKey); } - } -} - -@include build-theme-animations(ss-background, base, base); -@include build-theme-animations(ss-background, base100, base100); -@include build-theme-animations(ss-background, base200, base200); -@include build-theme-animations(ss-background, base300, base300); -@include build-theme-animations(primary, default, primary); -@include build-theme-animations(accent, default, accent); - -.action-mode-on-to-off-dark { - animation-name: actionModeOnToOffDark; - @include theme-animation-properties; -} -.action-mode-off-to-on-dark { - animation-name: actionModeOffToOnDark; - @include theme-animation-properties; -} -.action-mode-on-to-off-light { - animation-name: actionModeOnToOffLight; - @include theme-animation-properties; -} -.action-mode-off-to-on-light { - animation-name: actionModeOffToOnLight; - @include theme-animation-properties; -} -@keyframes actionModeOnToOffDark { - 0% { background-color: mat-color(map-get($dark-theme, accent)); } - 100% { background-color: mat-color(map-get($dark-theme, primary)); } -} -@keyframes actionModeOffToOnDark { - 0% { background-color: mat-color(map-get($dark-theme, primary)); } - 100% { background-color: mat-color(map-get($dark-theme, accent)); } -} -@keyframes actionModeOnToOffLight { - 0% { background-color: mat-color(map-get($light-theme, accent)); } - 100% { background-color: mat-color(map-get($light-theme, primary)); } -} -@keyframes actionModeOffToOnLight { - 0% { background-color: mat-color(map-get($light-theme, primary)); } - 100% { background-color: mat-color(map-get($light-theme, accent)); } -} diff --git a/src/main.ts b/src/main.ts index c03e0fc9..e3e2c12a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,7 @@ import 'hammerjs'; import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { EditorModule } from 'app/pages/editor/editor.module'; +import { EditorModule } from 'app/modules/editor/editor.module'; import { environment } from 'environments/environment'; const script = document.createElement('script'); diff --git a/src/styles.scss b/src/styles.scss index 6de49b0d..67f1ddf4 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,3 +1,3 @@ /* You can add global styles to this file, and also import other style files */ -@import './app/pages/editor/styles/app.scss'; +@import './app/modules/editor/styles/app.scss'; diff --git a/src/test/PathUtil.ts b/src/test/PathUtil.ts index 860c3264..4dafecca 100644 --- a/src/test/PathUtil.ts +++ b/src/test/PathUtil.ts @@ -1,5 +1,5 @@ -import { Path, SvgChar } from 'app/pages/editor/model/paths'; -import { Matrix } from 'app/pages/editor/scripts/common'; +import { Path, SvgChar } from 'app/modules/editor/model/paths'; +import { Matrix } from 'app/modules/editor/scripts/common'; type PathOp = | 'RV'