From 82b1b6928a37773a41f6408aa0ad31ced43428ea Mon Sep 17 00:00:00 2001 From: Dmitry Demensky Date: Mon, 13 Dec 2021 23:12:51 +0200 Subject: [PATCH 1/2] chore: install prettier --- .prettierrc.json | 4 ++++ package-lock.json | 6 ++++++ package.json | 1 + 3 files changed, 11 insertions(+) create mode 100644 .prettierrc.json diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..7c4168df --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "bracketSpacing": false, + "htmlWhitespaceSensitivity": "ignore" +} diff --git a/package-lock.json b/package-lock.json index caa3a110..572342fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11499,6 +11499,12 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prettier": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", + "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "dev": true + }, "pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", diff --git a/package.json b/package.json index 3b032d06..78910fe7 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "karma-coverage": "~2.0.3", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", + "prettier": "^2.5.1", "typescript": "~4.2.4" } } From 758042e709102921948cc12b1af77fcbd6c3e7da Mon Sep 17 00:00:00 2001 From: Dmitry Demensky Date: Mon, 13 Dec 2021 23:16:56 +0200 Subject: [PATCH 2/2] style: reformat all code with prettier --- .eslintrc.json | 21 +- .prettierrc.json | 3 +- README.md | 12 + angular.json | 14 +- docker-compose.yml | 4 +- karma.conf.js | 13 +- src/app/app.component.html | 1150 +++++++++------- src/app/app.component.scss | 252 ++-- src/app/app.component.spec.ts | 26 +- src/app/app.component.ts | 254 ++-- src/app/app.module.ts | 96 +- src/app/canvas/canvas.component.css | 87 +- src/app/canvas/canvas.component.html | 486 ++++--- src/app/canvas/canvas.component.spec.ts | 9 +- src/app/canvas/canvas.component.ts | 243 +++- src/app/config.service.spec.ts | 4 +- src/app/config.service.ts | 27 +- src/app/expandable/expandable.component.html | 18 +- src/app/expandable/expandable.component.scss | 52 +- .../expandable/expandable.component.spec.ts | 21 +- src/app/expandable/expandable.component.ts | 11 +- src/app/export/export-dialog.component.html | 105 +- src/app/export/export-dialog.component.scss | 31 +- src/app/export/export.component.html | 15 +- src/app/export/export.component.spec.ts | 15 +- src/app/export/export.component.ts | 52 +- src/app/formatter/formatter.directive.spec.ts | 4 +- src/app/formatter/formatter.directive.ts | 43 +- src/app/image.ts | 14 +- .../keyboard-navigable.directive.spec.ts | 6 +- .../keyboard-navigable.directive.ts | 27 +- src/app/open/open-dialog.component.css | 16 +- src/app/open/open-dialog.component.html | 75 +- src/app/open/open.component.html | 21 +- src/app/open/open.component.spec.ts | 15 +- src/app/open/open.component.ts | 73 +- src/app/save/save-dialog.component.html | 25 +- src/app/save/save.component.html | 19 +- src/app/save/save.component.spec.ts | 15 +- src/app/save/save.component.ts | 21 +- src/app/storage.service.spec.ts | 4 +- src/app/storage.service.ts | 16 +- src/app/svg-parser.spec.ts | 18 +- src/app/svg-parser.ts | 140 +- src/app/svg.spec.ts | 57 +- src/app/svg.ts | 1208 +++++++++-------- .../upload-image-dialog.component.html | 49 +- .../upload-image-dialog.component.scss | 42 +- .../upload-image/upload-image.component.html | 15 +- .../upload-image.component.spec.ts | 15 +- .../upload-image/upload-image.component.ts | 40 +- src/custom-theme.scss | 7 +- src/environments/environment.prod.ts | 2 +- src/environments/environment.ts | 2 +- src/index.html | 76 +- src/main.ts | 13 +- src/polyfills.ts | 3 +- src/styles.scss | 219 ++- src/test.ts | 10 +- tsconfig.app.json | 9 +- tsconfig.json | 5 +- tsconfig.spec.json | 14 +- 62 files changed, 3103 insertions(+), 2256 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 1e4624e6..f9cbe2de 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,18 +1,11 @@ { "root": true, - "ignorePatterns": [ - "projects/**/*" - ], + "ignorePatterns": ["projects/**/*"], "overrides": [ { - "files": [ - "*.ts" - ], + "files": ["*.ts"], "parserOptions": { - "project": [ - "tsconfig.json", - "e2e/tsconfig.json" - ], + "project": ["tsconfig.json", "e2e/tsconfig.json"], "createDefaultProgram": true }, "extends": [ @@ -39,12 +32,8 @@ } }, { - "files": [ - "*.html" - ], - "extends": [ - "plugin:@angular-eslint/template/recommended" - ], + "files": ["*.html"], + "extends": ["plugin:@angular-eslint/template/recommended"], "rules": {} } ] diff --git a/.prettierrc.json b/.prettierrc.json index 7c4168df..cde5b454 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -1,4 +1,5 @@ { "bracketSpacing": false, - "htmlWhitespaceSensitivity": "ignore" + "htmlWhitespaceSensitivity": "ignore", + "singleQuote": true } diff --git a/README.md b/README.md index fd182d12..e04b81da 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,19 @@ # SvgPathEditor + Edit or create SVG paths in browser: https://yqnn.github.io/svg-path-editor/ [![Image of Yaktocat](./doc/screenshot.png)](https://yqnn.github.io/svg-path-editor/) ## How to Use ##### Basic: + - Paste or edit the raw path in the **path** field - Click on **+** to add a new command to the path, select a type, then click on the destination - Move points with drag and drop - Click on a point, then on the **...** button to insert a command right after the selected one, to remove it, or to change its type ##### Commands panel: + - Click on the command type to toggle between relative and absolute coordinates - Relative command types are **orange**, absolute are **purple** - Click on **...** then **Delete** to delete a command @@ -18,18 +21,21 @@ Edit or create SVG paths in browser: https://yqnn.github.io/svg-path-editor/ - Click on **...** then **Convert to** to convert the selected command to a new type ##### ViewBox: + - Use mouse wheel, or click **Zoom in** and **Zoom out** to zoom in/out - Use drag & drop to move the viewBox - Click on **Zoom to Fit** to automatically set the viewBox depending on current path - ViewBox can also be set manually with the **x**, **y**, **width** and **height** fields ##### Path operations: + - Scale the full path with the **Scale** button - Translate the full path with the **Translate** button - Round all coordinates of the current path with the **Round** button - Convert all commands to relative or absolute coordinates with **Convert to relative** or **Convert to absolute** button ##### Shortcuts: + - Press **m**, **l**, **v**, **h**, **c**, **s**, **q**, **t**, **a** or **z** to insert a command after the selected one - Press **shift** + **m**, **l**, **v**, **h**, **c**, **s**, **q**, **t**, **a** or **z** to convert selected command to a new type - Press **echap** to delete the command being created, or the undo the current dragging operation @@ -43,21 +49,27 @@ Edit or create SVG paths in browser: https://yqnn.github.io/svg-path-editor/ ### With Node.js ##### Requirements + - [Node.js](https://nodejs.org/) v12.14, v14.15 or higher. ##### Dependencies + Run `npm install` to retrieve all the depencies of the project. ##### Development server + Run `npm start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. ##### Build + Run `npm run build` to build the project. The build artifacts will be stored in the `dist/` directory. ##### Running unit tests + Run `npm test` to execute the unit tests via [Karma](https://karma-runner.github.io). ### With Docker + Run `docker build -t svg-path-editor . && docker run -p 4200:4200 svg-path-editor` or `docker-compose up`. --- diff --git a/angular.json b/angular.json index 40b979b3..ada3ea8b 100644 --- a/angular.json +++ b/angular.json @@ -31,10 +31,7 @@ "src/assets", "src/manifest.webmanifest" ], - "styles": [ - "src/custom-theme.scss", - "src/styles.scss" - ], + "styles": ["src/custom-theme.scss", "src/styles.scss"], "scripts": [], "serviceWorker": true, "ngswConfigPath": "ngsw-config.json" @@ -103,19 +100,14 @@ "src/assets", "src/manifest.webmanifest" ], - "styles": [ - "src/styles.scss" - ], + "styles": ["src/styles.scss"], "scripts": [] } }, "lint": { "builder": "@angular-eslint/builder:lint", "options": { - "lintFilePatterns": [ - "src/**/*.ts", - "src/**/*.html" - ] + "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"] } } } diff --git a/docker-compose.yml b/docker-compose.yml index c241f565..cd8b9926 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: "3" +version: '3' services: web-server: build: . @@ -6,7 +6,7 @@ services: - 4200:4200 volumes: - .:/app - command: + command: - bash - -c - | diff --git a/karma.conf.js b/karma.conf.js index e566dab8..e43fed7c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -10,7 +10,7 @@ module.exports = function (config) { require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage'), - require('@angular-devkit/build-angular/plugins/karma') + require('@angular-devkit/build-angular/plugins/karma'), ], client: { jasmine: { @@ -19,18 +19,15 @@ module.exports = function (config) { // for example, you can disable the random execution with `random: false` // or set a specific seed with `seed: 4321` }, - clearContext: false // leave Jasmine Spec Runner output visible in browser + clearContext: false, // leave Jasmine Spec Runner output visible in browser }, jasmineHtmlReporter: { - suppressAll: true // removes the duplicated traces + suppressAll: true, // removes the duplicated traces }, coverageReporter: { dir: require('path').join(__dirname, './coverage/svg-path-editor'), subdir: '.', - reporters: [ - { type: 'html' }, - { type: 'text-summary' } - ] + reporters: [{type: 'html'}, {type: 'text-summary'}], }, reporters: ['progress', 'kjhtml'], port: 9876, @@ -39,6 +36,6 @@ module.exports = function (config) { autoWatch: true, browsers: ['Chrome'], singleRun: false, - restartOnFileChange: true + restartOnFileChange: true, }); }; diff --git a/src/app/app.component.html b/src/app/app.component.html index 3f367c90..362eb03d 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,488 +1,718 @@ -
-
- -
- -
-
- - -
-
-
- - - - - - - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
+
+
+ +
+ +
+
+ + +
+
+
+ + - -
-
- Snap to Grid -
- - -
-
-
- Show Ticks -
- - -
-
-
- Fill - Preview - Minify output -
+ -
+ +
+ - -
-
- - -
-
- - -
- -
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
-
-
- - -
-
- - -
- -
- -
-
- - -
- -
-
- - -
- + +
+
+ + Snap to Grid + +
+ + +
+
+
+ + Show Ticks + +
+ + +
+
+
+ + Fill + + + Preview + + + Minify output + +
+
+ +
+
+ + +
+
+ + +
+ +
- -
-
{{item.getType()}}
- - - - +
+
+ + +
+
+ + +
+ +
-
- -
-
-
+
+
+ + +
+
-
+
+ + +
+ -
- - +
+ {{ item.getType() }} +
+ + + + - - + +
+ +
+
+
-
- - +
+ + - + - +
+ + - -
-
- - - - -
+ - + + + +
+
+ + + +
-
+ +
+
- - Bug reports & feedback + + + Bug reports & feedback - - - - - - - + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + - - -
- opacity - Opacity -
- - {{image.opacity}} -
-
- - -
-
\ No newline at end of file + +
+ opacity + Opacity +
+ + {{ image.opacity }} +
+
+ + +
+
diff --git a/src/app/app.component.scss b/src/app/app.component.scss index cecdb237..d7924adf 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -1,170 +1,170 @@ :host { - display: block; - height: 100%; + display: block; + height: 100%; } - * { - box-sizing: border-box; + box-sizing: border-box; } .footer { - text-decoration: none; - position: fixed; - left:0; - height:28px; - width: 300px; - bottom:0px; - background-color: #111111; - color:#AAAAAA; - font-size: 14px; - display: flex; - justify-content: center; - align-items: center; - fill:#AAAAAA; - .mat-icon { - width:20px; - height: 20px; - margin:4px 8px 4px 0; - } - &:hover { - color:white; - fill:white; - text-decoration: underline; - } + text-decoration: none; + position: fixed; + left: 0; + height: 28px; + width: 300px; + bottom: 0px; + background-color: #111111; + color: #aaaaaa; + font-size: 14px; + display: flex; + justify-content: center; + align-items: center; + fill: #aaaaaa; + .mat-icon { + width: 20px; + height: 20px; + margin: 4px 8px 4px 0; + } + &:hover { + color: white; + fill: white; + text-decoration: underline; + } } .spacer { - height:32px; + height: 32px; } - /* UI */ .left-column-wrapper { - width:300px; - position: relative; + width: 300px; + position: relative; } .left-column { - width:300px; - overflow:auto; - background-color:#252526; - height: calc(100% - 28px); + width: 300px; + overflow: auto; + background-color: #252526; + height: calc(100% - 28px); } -.left-column-close, .left-column-open { - position: absolute; - top:8px; - border-radius: 0; - min-width:16px; - padding:8px 2px; - z-index:10; +.left-column-close, +.left-column-open { + position: absolute; + top: 8px; + border-radius: 0; + min-width: 16px; + padding: 8px 2px; + z-index: 10; } .left-column-close { - right:-28px; + right: -28px; } .left-column-open { - left:0; + left: 0; } input.svg-value { - width:38px; - text-align: center; - font-size: 10px; - margin-left:1px; - margin-right:1px; - padding-left:1px; - padding-right:1px; + width: 38px; + text-align: center; + font-size: 10px; + margin-left: 1px; + margin-right: 1px; + padding-left: 1px; + padding-right: 1px; } .type-token { - margin:0 1px; - font-size: 12px; - line-height: 20px; - height: 20px; - width: 20px; - text-align: center; - background-color:rgba(255, 128, 0, 0.5); - color:#c8c8c8; - border-style: solid; - border-width: 0 0 1px 0; - border-radius: 2px 2px 0 0; - border-color: #c8c8c8; - cursor: pointer; + margin: 0 1px; + font-size: 12px; + line-height: 20px; + height: 20px; + width: 20px; + text-align: center; + background-color: rgba(255, 128, 0, 0.5); + color: #c8c8c8; + border-style: solid; + border-width: 0 0 1px 0; + border-radius: 2px 2px 0 0; + border-color: #c8c8c8; + cursor: pointer; } .type-token-relative { - background-color:rgba(128, 0, 255, 0.5); + background-color: rgba(128, 0, 255, 0.5); } -.row>.type-token { - flex-shrink: 0; +.row > .type-token { + flex-shrink: 0; } div.hovered { - background-color: #37373d; + background-color: #37373d; } div.dragged { - background-color:#094771; + background-color: #094771; } /* Drawings */ .drawings { - flex:1; - align-self:stretch; - position: relative; - overflow:hidden; + flex: 1; + align-self: stretch; + position: relative; + overflow: hidden; - &:hover .add-button, .add-button.opened { - display: block; - } - .add-button { - display: none; - position: absolute; - font-size: 1px; - } + &:hover .add-button, + .add-button.opened { + display: block; + } + .add-button { + display: none; + position: absolute; + font-size: 1px; + } - .scene-top-actions { - position: absolute; - right: 12px; - top: 12px; - >button { - font-size: 1px; - margin-left:8px; - } - >.add-image-button { - position: absolute; - margin-left:8px; - top:0; - opacity: 0; - transition: top 0.2s, opacity 0.2s; - &.displayed { - top:48px; - opacity: 1; - } - } + .scene-top-actions { + position: absolute; + right: 12px; + top: 12px; + > button { + font-size: 1px; + margin-left: 8px; + } + > .add-image-button { + position: absolute; + margin-left: 8px; + top: 0; + opacity: 0; + transition: top 0.2s, opacity 0.2s; + &.displayed { + top: 48px; + opacity: 1; + } } + } - .scene-bottom-actions { - position: absolute; - right: 12px; - bottom: 12px; - width:40px; - >button { - font-size: 1px; - margin-top:8px; - } + .scene-bottom-actions { + position: absolute; + right: 12px; + bottom: 12px; + width: 40px; + > button { + font-size: 1px; + margin-top: 8px; } + } } @media screen and (max-width: 500px) { - .left-column { - width:100%; - } - .left-column-wrapper { - width:100% !important; - max-width:none !important; - } - .left-column-close { - right:0; - top: unset; - bottom: 4px; - padding:2px 6px; - } - .footer { - width: 100%; - } -} \ No newline at end of file + .left-column { + width: 100%; + } + .left-column-wrapper { + width: 100% !important; + max-width: none !important; + } + .left-column-close { + right: 0; + top: unset; + bottom: 4px; + padding: 2px 6px; + } + .footer { + width: 100%; + } +} diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 5f471546..7207bc89 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,20 +1,18 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed'; +import {TestBed, waitForAsync} from '@angular/core/testing'; +import {TestbedHarnessEnvironment} from '@angular/cdk/testing/testbed'; -import { AppComponent } from './app.component'; -import { AppModule } from './app.module'; +import {AppComponent} from './app.component'; +import {AppModule} from './app.module'; describe('AppComponent', () => { - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [ - AppModule - ], - declarations: [ - AppComponent - ], - }).compileComponents(); - })); + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [AppModule], + declarations: [AppComponent], + }).compileComponents(); + }) + ); it('should create the app', () => { const fixture = TestBed.createComponent(AppComponent); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 03267298..da5e5a9d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,29 +1,33 @@ -import { Component, AfterViewInit, HostListener, ViewChild } from '@angular/core'; -import { trigger, state, style, animate, transition } from '@angular/animations'; -import { Svg, SvgItem, Point, SvgPoint, SvgControlPoint, formatNumber } from './svg'; -import { MatIconRegistry } from '@angular/material/icon'; -import { DomSanitizer } from '@angular/platform-browser'; -import { StorageService } from './storage.service'; -import { CanvasComponent } from './canvas/canvas.component'; -import { Image } from './image'; -import { UploadImageComponent } from './upload-image/upload-image.component'; -import { ConfigService } from './config.service'; - +import {Component, AfterViewInit, HostListener, ViewChild} from '@angular/core'; +import {trigger, state, style, animate, transition} from '@angular/animations'; +import { + Svg, + SvgItem, + Point, + SvgPoint, + SvgControlPoint, + formatNumber, +} from './svg'; +import {MatIconRegistry} from '@angular/material/icon'; +import {DomSanitizer} from '@angular/platform-browser'; +import {StorageService} from './storage.service'; +import {CanvasComponent} from './canvas/canvas.component'; +import {Image} from './image'; +import {UploadImageComponent} from './upload-image/upload-image.component'; +import {ConfigService} from './config.service'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], animations: [ - trigger('leftColumnParent', [ - transition(':enter', []) - ]), + trigger('leftColumnParent', [transition(':enter', [])]), trigger('leftColumn', [ state('*', style({'max-width': '300px'})), transition(':enter', [style({'max-width': '0'}), animate('100ms ease')]), - transition(':leave', [animate('100ms ease', style({'max-width': '0'}))]) - ]) - ] + transition(':leave', [animate('100ms ease', style({'max-width': '0'}))]), + ]), + ], }) export class AppComponent implements AfterViewInit { // Svg path data model: @@ -32,10 +36,11 @@ export class AppComponent implements AfterViewInit { controlPoints: SvgControlPoint[] = []; // Raw path: - _rawPath = this.storage.getPath()?.path - || `M 4 8 L 10 1 L 13 0 L 12 3 L 5 9 C 6 10 6 11 7 10 C 7 11 8 12 7 12 A 1.42 1.42 0 0 1 6 13 ` - + `A 5 5 0 0 0 4 10 Q 3.5 9.9 3.5 10.5 T 2 11.8 T 1.2 11 T 2.5 9.5 T 3 9 A 5 5 90 0 0 0 7 A 1.42 1.42 0 0 1 1 6 ` - + `C 1 5 2 6 3 6 C 2 7 3 7 4 8 M 10 1 L 10 3 L 12 3 L 10.2 2.8 L 10 1`; + _rawPath = + this.storage.getPath()?.path || + `M 4 8 L 10 1 L 13 0 L 12 3 L 5 9 C 6 10 6 11 7 10 C 7 11 8 12 7 12 A 1.42 1.42 0 0 1 6 13 ` + + `A 5 5 0 0 0 4 10 Q 3.5 9.9 3.5 10.5 T 2 11.8 T 1.2 11 T 2.5 9.5 T 3 9 A 5 5 90 0 0 0 7 A 1.42 1.42 0 0 1 1 6 ` + + `C 1 5 2 6 3 6 C 2 7 3 7 4 8 M 10 1 L 10 3 L 12 3 L 10.2 2.8 L 10 1`; pathName: string = ''; invalidSyntax = false; @@ -60,7 +65,7 @@ export class AppComponent implements AfterViewInit { // Dragged & hovered elements draggedPoint: SvgPoint | null = null; focusedItem: SvgItem | null = null; - hoveredItem: SvgItem | null = null; + hoveredItem: SvgItem | null = null; wasCanvasDragged = false; draggedIsNew = false; dragging = false; @@ -85,8 +90,19 @@ export class AppComponent implements AfterViewInit { public cfg: ConfigService, private storage: StorageService ) { - for (const icon of ['delete', 'logo', 'more', 'github', 'zoom_in', 'zoom_out', 'zoom_fit']) { - matRegistry.addSvgIcon(icon, sanitizer.bypassSecurityTrustResourceUrl(`./assets/${icon}.svg`)); + for (const icon of [ + 'delete', + 'logo', + 'more', + 'github', + 'zoom_in', + 'zoom_out', + 'zoom_fit', + ]) { + matRegistry.addSvgIcon( + icon, + sanitizer.bypassSecurityTrustResourceUrl(`./assets/${icon}.svg`) + ); } this.parsedPath = new Svg(''); this.reloadPath(this.rawPath, true); @@ -95,32 +111,59 @@ export class AppComponent implements AfterViewInit { @HostListener('document:keydown', ['$event']) onKeyDown($event: any) { const tag = $event.target.tagName; if (tag !== 'INPUT' && tag !== 'TEXTAREA') { - if ($event.shiftKey && ($event.metaKey || $event.ctrlKey) && $event.key.toLowerCase() === 'z') { + if ( + $event.shiftKey && + ($event.metaKey || $event.ctrlKey) && + $event.key.toLowerCase() === 'z' + ) { this.redo(); $event.preventDefault(); - } else if (($event.metaKey || $event.ctrlKey) && $event.key.toLowerCase() === 'z') { + } else if ( + ($event.metaKey || $event.ctrlKey) && + $event.key.toLowerCase() === 'z' + ) { this.undo(); $event.preventDefault(); - } else if (!$event.metaKey && !$event.ctrlKey && /^[mlvhcsqtaz]$/i.test($event.key)) { + } else if ( + !$event.metaKey && + !$event.ctrlKey && + /^[mlvhcsqtaz]$/i.test($event.key) + ) { const isLower = $event.key === $event.key.toLowerCase(); const key = $event.key.toUpperCase(); - if (isLower && this.focusedItem && this.canInsertAfter(this.focusedItem, key)) { + if ( + isLower && + this.focusedItem && + this.canInsertAfter(this.focusedItem, key) + ) { this.insert(key, this.focusedItem, false); $event.preventDefault(); - } else if (!isLower && this.focusedItem && this.canConvert(this.focusedItem, key)) { + } else if ( + !isLower && + this.focusedItem && + this.canConvert(this.focusedItem, key) + ) { this.insert(key, this.focusedItem, true); $event.preventDefault(); } - } else if (!$event.metaKey && !$event.ctrlKey && $event.key === 'Escape') { + } else if ( + !$event.metaKey && + !$event.ctrlKey && + $event.key === 'Escape' + ) { if (this.dragging) { // If an element is being dragged, undo by reloading the current history entry this.reloadPath(this.history[this.historyCursor]); - } else if(this.canvas){ + } else if (this.canvas) { // stopDrag will unselect selected item if any this.canvas.stopDrag(); } $event.preventDefault(); - } else if (!$event.metaKey && !$event.ctrlKey && ($event.key === 'Delete' || $event.key === 'Backspace')) { + } else if ( + !$event.metaKey && + !$event.ctrlKey && + ($event.key === 'Delete' || $event.key === 'Backspace') + ) { if (this.focusedItem && this.canDelete(this.focusedItem)) { this.delete(this.focusedItem); $event.preventDefault(); @@ -133,8 +176,8 @@ export class AppComponent implements AfterViewInit { } } get decimals() { - return this.cfg.snapToGrid ? 0 : this.cfg.decimalPrecision; - } + return this.cfg.snapToGrid ? 0 : this.cfg.decimalPrecision; + } ngAfterViewInit() { setTimeout(() => { @@ -146,8 +189,8 @@ export class AppComponent implements AfterViewInit { return this._rawPath; } set rawPath(value: string) { - this._rawPath = value; - this.pushHistory(); + this._rawPath = value; + this.pushHistory(); } setIsDragging(dragging: boolean) { @@ -166,9 +209,16 @@ export class AppComponent implements AfterViewInit { } pushHistory() { - if (!this.historyDisabled && this.rawPath !== this.history[this.historyCursor]) { - this.historyCursor ++; - this.history.splice(this.historyCursor, this.history.length - this.historyCursor, this.rawPath); + if ( + !this.historyDisabled && + this.rawPath !== this.history[this.historyCursor] + ) { + this.historyCursor++; + this.history.splice( + this.historyCursor, + this.history.length - this.historyCursor, + this.rawPath + ); this.storage.addPath(null, this.rawPath); } } @@ -180,36 +230,44 @@ export class AppComponent implements AfterViewInit { undo() { if (this.canUndo()) { this.historyDisabled = true; - this.historyCursor --; + this.historyCursor--; this.reloadPath(this.history[this.historyCursor]); this.historyDisabled = false; } } canRedo(): boolean { - return this.historyCursor < this.history.length - 1 && !this.isEditingImages; + return ( + this.historyCursor < this.history.length - 1 && !this.isEditingImages + ); } redo() { if (this.canRedo()) { this.historyDisabled = true; - this.historyCursor ++; + this.historyCursor++; this.reloadPath(this.history[this.historyCursor]); this.historyDisabled = false; } } - updateViewPort(x: number, y: number, w: number | null, h: number | null, force = false) { + updateViewPort( + x: number, + y: number, + w: number | null, + h: number | null, + force = false + ) { if (!force && this.cfg.viewPortLocked) { return; } - if (w === null && h !==null) { - w = this.canvasWidth * h / this.canvasHeight; + if (w === null && h !== null) { + w = (this.canvasWidth * h) / this.canvasHeight; } - if (h === null && w !==null) { - h = this.canvasHeight * w / this.canvasWidth; + if (h === null && w !== null) { + h = (this.canvasHeight * w) / this.canvasWidth; } - if (!w || !h) { + if (!w || !h) { return; } @@ -222,8 +280,10 @@ export class AppComponent implements AfterViewInit { insert(type: string, after: SvgItem, convert: boolean) { if (convert) { - this.focusedItem = - this.parsedPath.changeType(after, after.relative ? type.toLowerCase() : type); + this.focusedItem = this.parsedPath.changeType( + after, + after.relative ? type.toLowerCase() : type + ); this.afterModelChange(); } else { this.draggedIsNew = true; @@ -243,32 +303,41 @@ export class AppComponent implements AfterViewInit { if (type.toLowerCase() !== 'm' || !newItem) { const relative = type.toLowerCase() === type; - const X = (relative ? 0 : point1.x).toString(); - const Y = (relative ? 0 : point1.y).toString(); + const X = (relative ? 0 : point1.x).toString(); + const Y = (relative ? 0 : point1.y).toString(); switch (type.toLocaleLowerCase()) { - case 'm': case 'l': case 't': - newItem = SvgItem.Make([type, X, Y]) ; break; + case 'm': + case 'l': + case 't': + newItem = SvgItem.Make([type, X, Y]); + break; case 'h': - newItem = SvgItem.Make([type, X]) ; break; + newItem = SvgItem.Make([type, X]); + break; case 'v': - newItem = SvgItem.Make([type, Y]) ; break; - case 's': case 'q': - newItem = SvgItem.Make([type, X , Y, X, Y]) ; break; + newItem = SvgItem.Make([type, Y]); + break; + case 's': + case 'q': + newItem = SvgItem.Make([type, X, Y, X, Y]); + break; case 'c': - newItem = SvgItem.Make([type, X , Y, X, Y, X, Y]) ; break; + newItem = SvgItem.Make([type, X, Y, X, Y, X, Y]); + break; case 'a': - newItem = SvgItem.Make([type, '1' , '1', '0', '0', '0', X, Y]) ; break; + newItem = SvgItem.Make([type, '1', '1', '0', '0', '0', X, Y]); + break; case 'z': newItem = SvgItem.Make([type]); } - if(newItem) { + if (newItem) { this.parsedPath.insert(newItem, after); } } this.setHistoryDisabled(true); this.afterModelChange(); - if(newItem) { + if (newItem) { this.focusedItem = newItem; this.draggedPoint = newItem.targetLocation(); } @@ -284,10 +353,10 @@ export class AppComponent implements AfterViewInit { let xmax = 10; let ymax = 10; if (this.targetPoints.length > 0) { - xmin = Math.min(...this.targetPoints.map( it => it.x )); - ymin = Math.min(...this.targetPoints.map( it => it.y )); - xmax = Math.max(...this.targetPoints.map( it => it.x )); - ymax = Math.max(...this.targetPoints.map( it => it.y )); + xmin = Math.min(...this.targetPoints.map((it) => it.x)); + ymin = Math.min(...this.targetPoints.map((it) => it.y)); + xmax = Math.max(...this.targetPoints.map((it) => it.x)); + ymax = Math.max(...this.targetPoints.map((it) => it.y)); } const k = this.canvasHeight / this.canvasWidth; let w = xmax - xmin + 2; @@ -298,22 +367,17 @@ export class AppComponent implements AfterViewInit { h = k * w; } - this.updateViewPort( - xmin - 1, - ymin - 1, - w, - h - ); + this.updateViewPort(xmin - 1, ymin - 1, w, h); } - scale(x: number, y: number) { + scale(x: number, y: number) { this.parsedPath.scale(1 * x, 1 * y); this.scaleX = 1; this.scaleY = 1; this.afterModelChange(); } - translate(x: number, y: number) { + translate(x: number, y: number) { this.parsedPath.translate(1 * x, 1 * y); this.translateX = 0; this.translateY = 0; @@ -353,12 +417,14 @@ export class AppComponent implements AfterViewInit { const idx = this.parsedPath.path.indexOf(item); return idx > 0; } - canInsertAfter(item: SvgItem | null, type: string): boolean { - let previousType: string | null = null; + canInsertAfter(item: SvgItem | null, type: string): boolean { + let previousType: string | null = null; if (item !== null) { previousType = item.getType().toUpperCase(); } else if (this.parsedPath.path.length > 0) { - previousType = this.parsedPath.path[this.parsedPath.path.length - 1].getType().toUpperCase(); + previousType = this.parsedPath.path[this.parsedPath.path.length - 1] + .getType() + .toUpperCase(); } if (!previousType) { return type !== 'Z'; @@ -369,16 +435,16 @@ export class AppComponent implements AfterViewInit { if (previousType === 'Z') { return type !== 'Z' && type !== 'T' && type !== 'S'; } - if (previousType === 'C' || previousType === 'S' ) { + if (previousType === 'C' || previousType === 'S') { return type !== 'T'; } - if (previousType === 'Q' || previousType === 'T' ) { + if (previousType === 'Q' || previousType === 'T') { return type !== 'S'; } return type !== 'T' && type !== 'S'; } canConvert(item: SvgItem, to: string): boolean { - const idx = this.parsedPath.path.indexOf(item) ; + const idx = this.parsedPath.path.indexOf(item); if (idx === 0) { return false; } @@ -406,8 +472,24 @@ export class AppComponent implements AfterViewInit { q: ['dx1', 'dy1', 'dx', 'dy'], T: ['x', 'y'], t: ['dx', 'dy'], - A: ['rx', 'ry', 'x-axis-rotation', 'large-arc-flag', 'sweep-flag', 'x', 'y'], - a: ['rx', 'ry', 'x-axis-rotation', 'large-arc-flag', 'sweep-flag', 'dx', 'dy'] + A: [ + 'rx', + 'ry', + 'x-axis-rotation', + 'large-arc-flag', + 'sweep-flag', + 'x', + 'y', + ], + a: [ + 'rx', + 'ry', + 'x-axis-rotation', + 'large-arc-flag', + 'sweep-flag', + 'dx', + 'dy', + ], }; return labels[item.getType()][idx]; } @@ -458,7 +540,7 @@ export class AppComponent implements AfterViewInit { } cancelAddImage(): void { - if(this.images.length === 0) { + if (this.images.length === 0) { this.isEditingImages = false; this.focusedImage = null; } @@ -473,13 +555,13 @@ export class AppComponent implements AfterViewInit { } } - focusItem(it: SvgItem | null): void { - if(it !== this.focusedItem) { + focusItem(it: SvgItem | null): void { + if (it !== this.focusedItem) { this.focusedItem = it; const idx = this.parsedPath.path.indexOf(this.focusedItem!); document.getElementById(`svg_command_row_${idx}`)?.scrollIntoView({ behavior: 'smooth', - block: 'nearest' + block: 'nearest', }); } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 54b86bf6..e6fb4cd7 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,35 +1,43 @@ -import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import {BrowserModule} from '@angular/platform-browser'; +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import { MatInputModule } from '@angular/material/input'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatMenuModule } from '@angular/material/menu'; -import { MatTooltipModule, MAT_TOOLTIP_SCROLL_STRATEGY } from '@angular/material/tooltip'; -import { MatSliderModule } from '@angular/material/slider'; -import { ScrollingModule } from '@angular/cdk/scrolling'; -import { Overlay, ScrollStrategy } from '@angular/cdk/overlay'; +import {MatInputModule} from '@angular/material/input'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatMenuModule} from '@angular/material/menu'; +import { + MatTooltipModule, + MAT_TOOLTIP_SCROLL_STRATEGY, +} from '@angular/material/tooltip'; +import {MatSliderModule} from '@angular/material/slider'; +import {ScrollingModule} from '@angular/cdk/scrolling'; +import {Overlay, ScrollStrategy} from '@angular/cdk/overlay'; - -import { AppComponent } from './app.component'; -import { HttpClientModule } from '@angular/common/http'; -import { ExpandableComponent } from './expandable/expandable.component'; -import { CanvasComponent } from './canvas/canvas.component'; -import { OpenComponent, OpenDialogComponent } from './open/open.component'; -import { SaveComponent, SaveDialogComponent } from './save/save.component'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatTableModule } from '@angular/material/table'; -import { MatSortModule } from '@angular/material/sort'; -import { FormatterDirective } from './formatter/formatter.directive'; -import { KeyboardNavigableDirective } from './keyboard-navigable/keyboard-navigable.directive'; -import { ExportComponent, ExportDialogComponent } from './export/export.component'; -import { UploadImageComponent, UploadImageDialogComponent } from './upload-image/upload-image.component'; -import { ServiceWorkerModule } from '@angular/service-worker'; -import { environment } from '../environments/environment'; +import {AppComponent} from './app.component'; +import {HttpClientModule} from '@angular/common/http'; +import {ExpandableComponent} from './expandable/expandable.component'; +import {CanvasComponent} from './canvas/canvas.component'; +import {OpenComponent, OpenDialogComponent} from './open/open.component'; +import {SaveComponent, SaveDialogComponent} from './save/save.component'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatTableModule} from '@angular/material/table'; +import {MatSortModule} from '@angular/material/sort'; +import {FormatterDirective} from './formatter/formatter.directive'; +import {KeyboardNavigableDirective} from './keyboard-navigable/keyboard-navigable.directive'; +import { + ExportComponent, + ExportDialogComponent, +} from './export/export.component'; +import { + UploadImageComponent, + UploadImageDialogComponent, +} from './upload-image/upload-image.component'; +import {ServiceWorkerModule} from '@angular/service-worker'; +import {environment} from '../environments/environment'; @NgModule({ declarations: [ @@ -45,7 +53,7 @@ import { environment } from '../environments/environment'; UploadImageComponent, UploadImageDialogComponent, FormatterDirective, - KeyboardNavigableDirective + KeyboardNavigableDirective, ], imports: [ BrowserModule, @@ -68,18 +76,18 @@ import { environment } from '../environments/environment'; enabled: environment.production, // Register the ServiceWorker as soon as the app is stable // or after 30 seconds (whichever comes first). - registrationStrategy: 'registerWhenStable:30000' - }) + registrationStrategy: 'registerWhenStable:30000', + }), + ], + providers: [ + { + provide: MAT_TOOLTIP_SCROLL_STRATEGY, + deps: [Overlay], + useFactory(overlay: Overlay): () => ScrollStrategy { + return () => overlay.scrollStrategies.close(); + }, + }, ], - providers: [{ - provide: MAT_TOOLTIP_SCROLL_STRATEGY, - deps: [Overlay], - useFactory(overlay: Overlay): () => ScrollStrategy { - return () => overlay.scrollStrategies.close(); - } - }], - bootstrap: [AppComponent] + bootstrap: [AppComponent], }) -export class AppModule { } - - +export class AppModule {} diff --git a/src/app/canvas/canvas.component.css b/src/app/canvas/canvas.component.css index c1eb888a..8896f6e6 100644 --- a/src/app/canvas/canvas.component.css +++ b/src/app/canvas/canvas.component.css @@ -1,113 +1,110 @@ :host { - background-color:#1e1e1e; + background-color: #1e1e1e; } path { - stroke:white; - fill:none; + stroke: white; + fill: none; } path.filled { - fill:rgba(255,255,255,0.25); + fill: rgba(255, 255, 255, 0.25); } path.hovered { - stroke: red; - fill: none; + stroke: red; + fill: none; } path.dragged { - stroke: rgb(0, 174, 255); - fill: none; + stroke: rgb(0, 174, 255); + fill: none; } line.grid { - stroke:#353536; + stroke: #353536; } line.grid.tick { - stroke: #454545; + stroke: #454545; } line.control { - stroke:gray; + stroke: gray; } line.hovered { - stroke:red; + stroke: red; } circle { - cursor: pointer; - stroke: transparent; + cursor: pointer; + stroke: transparent; } circle.control { - fill:gray; + fill: gray; } circle.target { - fill:white; + fill: white; } circle.hovered { - fill:red; + fill: red; } circle.dragged { - fill:rgb(0, 174, 255); + fill: rgb(0, 174, 255); } :host.preview { - background-color:white; + background-color: white; } :host.preview path { - stroke:black; + stroke: black; } :host.preview path.filled { - fill:black; - stroke:none; + fill: black; + stroke: none; } :host.preview path.hovered { - stroke: red; - fill: none; + stroke: red; + fill: none; } :host.preview circle, :host.preview line { - display: none; + display: none; } text.tick { - fill: #595959; - text-anchor: end; - /* Prevent accidental selection of axis markers on drag */ - cursor: default; - user-select: none; + fill: #595959; + text-anchor: end; + /* Prevent accidental selection of axis markers on drag */ + cursor: default; + user-select: none; } - - - /* images */ image { - pointer-events: none; + pointer-events: none; } rect.image-control { - fill: rgba(0,0,0,0); - stroke: none; + fill: rgba(0, 0, 0, 0); + stroke: none; } line.image-control { - stroke:gray; + stroke: gray; } circle.image-control { - fill:gray; + fill: gray; } .focused-image line.image-control { - stroke:rgb(0, 174, 255); + stroke: rgb(0, 174, 255); } .focused-image circle.image-control { - fill:rgb(0, 174, 255); + fill: rgb(0, 174, 255); } .image-control.horizontal { - cursor: ns-resize; + cursor: ns-resize; } .image-control.vertical { - cursor: ew-resize; + cursor: ew-resize; } .image-control.down { - cursor: nwse-resize; + cursor: nwse-resize; } .image-control.up { - cursor: nesw-resize; + cursor: nesw-resize; } .image-control.move { - cursor: move; + cursor: move; } diff --git a/src/app/canvas/canvas.component.html b/src/app/canvas/canvas.component.html index 5dd18844..6d3bc5bd 100644 --- a/src/app/canvas/canvas.component.html +++ b/src/app/canvas/canvas.component.html @@ -1,206 +1,312 @@ - - - - + + + + - - - {{ y }} - + + + + {{ y }} + + - - {{ x }} - + + + {{ x }} + + - + - - - - + + + + + - + + - - - - - - - - + /> + - + - - - + - - - + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/src/app/canvas/canvas.component.spec.ts b/src/app/canvas/canvas.component.spec.ts index ea2e3ad0..a78fd669 100644 --- a/src/app/canvas/canvas.component.spec.ts +++ b/src/app/canvas/canvas.component.spec.ts @@ -1,6 +1,6 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; -import { CanvasComponent } from './canvas.component'; +import {CanvasComponent} from './canvas.component'; describe('CanvasComponent', () => { let component: CanvasComponent; @@ -8,9 +8,8 @@ describe('CanvasComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ CanvasComponent ] - }) - .compileComponents(); + declarations: [CanvasComponent], + }).compileComponents(); }); beforeEach(() => { diff --git a/src/app/canvas/canvas.component.ts b/src/app/canvas/canvas.component.ts index e18654cc..19d360e4 100644 --- a/src/app/canvas/canvas.component.ts +++ b/src/app/canvas/canvas.component.ts @@ -1,35 +1,79 @@ -import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; -import { Subject } from 'rxjs'; -import { buffer, map, throttleTime } from 'rxjs/operators'; -import { Image } from '../image'; -import { Point, Svg, SvgControlPoint, SvgItem, SvgPoint } from '../svg'; +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + HostListener, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild, +} from '@angular/core'; +import {Subject} from 'rxjs'; +import {buffer, map, throttleTime} from 'rxjs/operators'; +import {Image} from '../image'; +import {Point, Svg, SvgControlPoint, SvgItem, SvgPoint} from '../svg'; /* eslint-disable @angular-eslint/component-selector */ @Component({ selector: '[app-canvas]', templateUrl: './canvas.component.html', - styleUrls: ['./canvas.component.css'] + styleUrls: ['./canvas.component.css'], }) export class CanvasComponent implements OnInit, OnChanges, AfterViewInit { - get canvasWidth(): number { return this._canvasWidth; } - set canvasWidth(canvasWidth: number) { this._canvasWidth = canvasWidth; this.canvasWidthChange.emit(this._canvasWidth); } - get canvasHeight(): number { return this._canvasHeight; } - set canvasHeight(canvasHeight: number) { this._canvasHeight = canvasHeight; this.canvasHeightChange.emit(this._canvasHeight); } - get draggedPoint(): SvgPoint | null { return this._draggedPoint; } - @Input() set draggedPoint(draggedPoint: SvgPoint| null ) { this._draggedPoint = draggedPoint; this.draggedPointChange.emit(this.draggedPoint); } - get focusedItem(): SvgItem | null { return this._focusedItem; } - @Input() set focusedItem(focusedItem: SvgItem | null) { this._focusedItem = focusedItem; this.focusedItemChange.emit(this.focusedItem); } - get hoveredItem(): SvgItem | null { return this._hoveredItem; } - @Input() set hoveredItem(hoveredItem: SvgItem | null ) { this._hoveredItem = hoveredItem; this.hoveredItemChange.emit(this.hoveredItem); } - get wasCanvasDragged(): boolean { return this._wasCanvasDragged; } + get canvasWidth(): number { + return this._canvasWidth; + } + set canvasWidth(canvasWidth: number) { + this._canvasWidth = canvasWidth; + this.canvasWidthChange.emit(this._canvasWidth); + } + get canvasHeight(): number { + return this._canvasHeight; + } + set canvasHeight(canvasHeight: number) { + this._canvasHeight = canvasHeight; + this.canvasHeightChange.emit(this._canvasHeight); + } + get draggedPoint(): SvgPoint | null { + return this._draggedPoint; + } + @Input() set draggedPoint(draggedPoint: SvgPoint | null) { + this._draggedPoint = draggedPoint; + this.draggedPointChange.emit(this.draggedPoint); + } + get focusedItem(): SvgItem | null { + return this._focusedItem; + } + @Input() set focusedItem(focusedItem: SvgItem | null) { + this._focusedItem = focusedItem; + this.focusedItemChange.emit(this.focusedItem); + } + get hoveredItem(): SvgItem | null { + return this._hoveredItem; + } + @Input() set hoveredItem(hoveredItem: SvgItem | null) { + this._hoveredItem = hoveredItem; + this.hoveredItemChange.emit(this.hoveredItem); + } + get wasCanvasDragged(): boolean { + return this._wasCanvasDragged; + } @Input() set wasCanvasDragged(wasCanvasDragged: boolean) { this._wasCanvasDragged = wasCanvasDragged; this.wasCanvasDraggedChange.emit(this._wasCanvasDragged); } - get focusedImage(): Image | null { return this._focusedImage; } - @Input() set focusedImage(focusedImage: Image | null) { this._focusedImage = focusedImage; this.focusedImageChange.emit(this.focusedImage); } + get focusedImage(): Image | null { + return this._focusedImage; + } + @Input() set focusedImage(focusedImage: Image | null) { + this._focusedImage = focusedImage; + this.focusedImageChange.emit(this.focusedImage); + } - constructor(public canvas: ElementRef) { } + constructor(public canvas: ElementRef) {} @Input() parsedPath?: Svg; @Input() targetPoints: SvgPoint[] = []; @Input() controlPoints: SvgControlPoint[] = []; @@ -50,8 +94,13 @@ export class CanvasComponent implements OnInit, OnChanges, AfterViewInit { @Output() afterModelChange = new EventEmitter(); @Output() dragging = new EventEmitter(); - @Output() viewPort = new EventEmitter<{x: number, y: number, w: number, h: number | null, force?: boolean}>(); - + @Output() viewPort = new EventEmitter<{ + x: number; + y: number; + w: number; + h: number | null; + force?: boolean; + }>(); @Output() emptyCanvas = new EventEmitter(); @@ -90,7 +139,12 @@ export class CanvasComponent implements OnInit, OnChanges, AfterViewInit { trackByIndex = (idx: number, _: any) => idx; ngOnChanges(changes: SimpleChanges): void { - if (changes.viewPortX || changes.viewPortY || changes.viewPortWidth || changes.viewPortHeight) { + if ( + changes.viewPortX || + changes.viewPortY || + changes.viewPortWidth || + changes.viewPortHeight + ) { this.refreshGrid(); } if (changes.draggedPoint && changes.draggedPoint.currentValue) { @@ -111,13 +165,18 @@ export class CanvasComponent implements OnInit, OnChanges, AfterViewInit { } ngOnInit(): void { - const throttler = throttleTime(20, undefined, {leading: false, trailing: true}); + const throttler = throttleTime(20, undefined, { + leading: false, + trailing: true, + }); this.wheel$ - .pipe( buffer(this.wheel$.pipe(throttler)) ) - .pipe( map(ev => ({ + .pipe(buffer(this.wheel$.pipe(throttler))) + .pipe( + map((ev) => ({ event: ev[0], - deltaY: ev.reduce((acc, cur) => acc + cur.deltaY, 0) - }))) + deltaY: ev.reduce((acc, cur) => acc + cur.deltaY, 0), + })) + ) .subscribe(this.mousewheel.bind(this)); } @@ -128,7 +187,7 @@ export class CanvasComponent implements OnInit, OnChanges, AfterViewInit { @HostListener('mousemove', ['$event']) onMouseMove($event: MouseEvent) { this.drag($event); } - @HostListener('mouseup', ['$event']) onMouseUp($event: MouseEvent) { + @HostListener('mouseup', ['$event']) onMouseUp($event: MouseEvent) { this.stopDrag(); } @HostListener('touchstart', ['$event']) onTouchStart($event: TouchEvent) { @@ -149,7 +208,6 @@ export class CanvasComponent implements OnInit, OnChanges, AfterViewInit { this.hoveredItem = null; } - refreshCanvasSize(emitEmptyCanvas = false) { const rect = this.canvas.nativeElement.parentNode.getBoundingClientRect(); if (rect.width === 0 && emitEmptyCanvas) { @@ -163,21 +221,28 @@ export class CanvasComponent implements OnInit, OnChanges, AfterViewInit { y: this.viewPortY, w: this.viewPortWidth, h: null, - force: true + force: true, }); } refreshGrid() { if (5 * this.viewPortWidth <= this.canvasWidth) { - this.xGrid = Array(Math.ceil(this.viewPortWidth) + 1).fill(null).map((_, i) => Math.floor(this.viewPortX) + i); - this.yGrid = Array(Math.ceil(this.viewPortHeight) + 1).fill(null).map((_, i) => Math.floor(this.viewPortY) + i); + this.xGrid = Array(Math.ceil(this.viewPortWidth) + 1) + .fill(null) + .map((_, i) => Math.floor(this.viewPortX) + i); + this.yGrid = Array(Math.ceil(this.viewPortHeight) + 1) + .fill(null) + .map((_, i) => Math.floor(this.viewPortY) + i); } else { this.xGrid = []; this.yGrid = []; } } - eventToLocation(event: MouseEvent | TouchEvent, idx = 0): {x: number, y: number} { + eventToLocation( + event: MouseEvent | TouchEvent, + idx = 0 + ): {x: number; y: number} { const rect = this.canvas.nativeElement.getBoundingClientRect(); const touch = event instanceof MouseEvent ? event : event.touches[idx]; const x = this.viewPortX + (touch.clientX - rect.left) * this.strokeWidth; @@ -185,43 +250,62 @@ export class CanvasComponent implements OnInit, OnChanges, AfterViewInit { return {x, y}; } - pinchToZoom(previousEvent: MouseEvent | TouchEvent, event: MouseEvent | TouchEvent) { - if ( window.TouchEvent - && previousEvent instanceof TouchEvent - && event instanceof TouchEvent - && previousEvent.touches.length >= 2 - && event.touches.length >= 2) - { + pinchToZoom( + previousEvent: MouseEvent | TouchEvent, + event: MouseEvent | TouchEvent + ) { + if ( + window.TouchEvent && + previousEvent instanceof TouchEvent && + event instanceof TouchEvent && + previousEvent.touches.length >= 2 && + event.touches.length >= 2 + ) { const pt = this.eventToLocation(event, 0); const pt2 = this.eventToLocation(event, 1); const oriPt = this.eventToLocation(previousEvent, 0); const oriPt2 = this.eventToLocation(previousEvent, 1); - const ptm = {x: 0.5 * (pt.x + pt2.x) , y: 0.5 * (pt.y + pt2.y)}; - const oriPtm = {x: 0.5 * (oriPt.x + oriPt2.x) , y: 0.5 * (oriPt.y + oriPt2.y)}; - const delta = {x: oriPtm.x - ptm.x , y: oriPtm.y - ptm.y}; + const ptm = {x: 0.5 * (pt.x + pt2.x), y: 0.5 * (pt.y + pt2.y)}; + const oriPtm = { + x: 0.5 * (oriPt.x + oriPt2.x), + y: 0.5 * (oriPt.y + oriPt2.y), + }; + const delta = {x: oriPtm.x - ptm.x, y: oriPtm.y - ptm.y}; const k = - Math.sqrt((oriPt.x - oriPt2.x) * (oriPt.x - oriPt2.x) + (oriPt.y - oriPt2.y) * (oriPt.y - oriPt2.y)) / - Math.sqrt((pt.x - pt2.x) * (pt.x - pt2.x) + (pt.y - pt2.y) * (pt.y - pt2.y)); + Math.sqrt( + (oriPt.x - oriPt2.x) * (oriPt.x - oriPt2.x) + + (oriPt.y - oriPt2.y) * (oriPt.y - oriPt2.y) + ) / + Math.sqrt( + (pt.x - pt2.x) * (pt.x - pt2.x) + (pt.y - pt2.y) * (pt.y - pt2.y) + ); return {zoom: k, delta, center: ptm}; } return null; } - mousewheel(event: {event: WheelEvent, deltaY: number}) { + mousewheel(event: {event: WheelEvent; deltaY: number}) { const scale = Math.pow(1.005, event.deltaY); const pt = this.eventToLocation(event.event); this.zoomViewPort(scale, pt); } - zoomViewPort(scale: number, pt?: {x: number, y: number}) { + zoomViewPort(scale: number, pt?: {x: number; y: number}) { if (!pt) { - pt = {x: this.viewPortX + 0.5 * this.viewPortWidth, y: this.viewPortY + 0.5 * this.viewPortHeight}; + pt = { + x: this.viewPortX + 0.5 * this.viewPortWidth, + y: this.viewPortY + 0.5 * this.viewPortHeight, + }; } const w = scale * this.viewPortWidth; const h = scale * this.viewPortHeight; - const x = this.viewPortX + ((pt.x - this.viewPortX) - scale * (pt.x - this.viewPortX)); - const y = this.viewPortY + ((pt.y - this.viewPortY) - scale * (pt.y - this.viewPortY)); + const x = + this.viewPortX + + (pt.x - this.viewPortX - scale * (pt.x - this.viewPortX)); + const y = + this.viewPortY + + (pt.y - this.viewPortY - scale * (pt.y - this.viewPortY)); this.viewPort.emit({x, y, w, h}); } @@ -245,7 +329,11 @@ export class CanvasComponent implements OnInit, OnChanges, AfterViewInit { this.dragWithoutClick = false; } - startDragImage(event: MouseEvent | TouchEvent, im: Image, type: number): void { + startDragImage( + event: MouseEvent | TouchEvent, + im: Image, + type: number + ): void { this.dragging.emit(true); this.draggedEvt = event; this.draggedImage = im; @@ -275,9 +363,12 @@ export class CanvasComponent implements OnInit, OnChanges, AfterViewInit { } drag(event: MouseEvent | TouchEvent) { - if (this.draggedPoint || this.draggedEvt || this.draggedImage) { - - if (!this.dragWithoutClick && event instanceof MouseEvent && event.buttons === 0) { + if (this.draggedPoint || this.draggedEvt || this.draggedImage) { + if ( + !this.dragWithoutClick && + event instanceof MouseEvent && + event.buttons === 0 + ) { // Stop dragging if click is not maintained anymore. this.stopDrag(); return; @@ -289,47 +380,61 @@ export class CanvasComponent implements OnInit, OnChanges, AfterViewInit { const oriPt = this.eventToLocation(this.draggedEvt); /* eslint-disable no-bitwise */ if (this.draggedImageType & 0b0001) { - this.draggedImage.x1 += (pt.x - oriPt.x); + this.draggedImage.x1 += pt.x - oriPt.x; } if (this.draggedImageType & 0b0010) { - this.draggedImage.y1 += (pt.y - oriPt.y); + this.draggedImage.y1 += pt.y - oriPt.y; } if (this.draggedImageType & 0b0100) { - this.draggedImage.x2 += (pt.x - oriPt.x); + this.draggedImage.x2 += pt.x - oriPt.x; } if (this.draggedImageType & 0b1000) { - this.draggedImage.y2 += (pt.y - oriPt.y); + this.draggedImage.y2 += pt.y - oriPt.y; } /* eslint-enable no-bitwise */ this.draggedEvt = event; - } else if (this.draggedPoint && this.parsedPath) { - const decimals = event.ctrlKey ? (this.decimals ? 0 : 3) : this.decimals; + const decimals = event.ctrlKey + ? this.decimals + ? 0 + : 3 + : this.decimals; pt.x = parseFloat(pt.x.toFixed(decimals)); pt.y = parseFloat(pt.y.toFixed(decimals)); this.parsedPath.setLocation(this.draggedPoint, pt as Point); if (this.draggedIsNew) { - const previousIdx = this.parsedPath.path.indexOf(this.draggedPoint.itemReference) - 1; + const previousIdx = + this.parsedPath.path.indexOf(this.draggedPoint.itemReference) - 1; if (previousIdx >= 0) { - this.draggedPoint.itemReference.resetControlPoints(this.parsedPath.path[previousIdx]); + this.draggedPoint.itemReference.resetControlPoints( + this.parsedPath.path[previousIdx] + ); } } this.afterModelChange.emit(); this.draggedEvt = null; - } else if(this.draggedEvt) { + } else if (this.draggedEvt) { this.wasCanvasDragged = true; const pinchToZoom = this.pinchToZoom(this.draggedEvt, event); - if (pinchToZoom !== null){ + if (pinchToZoom !== null) { const w = pinchToZoom.zoom * this.viewPortWidth; const h = pinchToZoom.zoom * this.viewPortHeight; - const x = this.viewPortX + pinchToZoom.delta.x + (pinchToZoom.center.x - this.viewPortX) * (1 - pinchToZoom.zoom); - const y = this.viewPortY + pinchToZoom.delta.y + (pinchToZoom.center.y - this.viewPortY) * (1 - pinchToZoom.zoom); + const x = + this.viewPortX + + pinchToZoom.delta.x + + (pinchToZoom.center.x - this.viewPortX) * (1 - pinchToZoom.zoom); + const y = + this.viewPortY + + pinchToZoom.delta.y + + (pinchToZoom.center.y - this.viewPortY) * (1 - pinchToZoom.zoom); this.viewPort.emit({x, y, w, h}); } else { const oriPt = this.eventToLocation(this.draggedEvt); this.viewPort.emit({ - x: this.viewPortX + (oriPt.x - pt.x), y: this.viewPortY + (oriPt.y - pt.y), - w: this.viewPortWidth, h: this.viewPortHeight + x: this.viewPortX + (oriPt.x - pt.x), + y: this.viewPortY + (oriPt.y - pt.y), + w: this.viewPortWidth, + h: this.viewPortHeight, }); } this.draggedEvt = event; diff --git a/src/app/config.service.spec.ts b/src/app/config.service.spec.ts index aca64dd3..2264bb76 100644 --- a/src/app/config.service.spec.ts +++ b/src/app/config.service.spec.ts @@ -1,6 +1,6 @@ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { ConfigService } from './config.service'; +import {ConfigService} from './config.service'; describe('ConfigService', () => { let service: ConfigService; diff --git a/src/app/config.service.ts b/src/app/config.service.ts index 67464373..f18c3072 100644 --- a/src/app/config.service.ts +++ b/src/app/config.service.ts @@ -1,9 +1,7 @@ -import { Injectable } from '@angular/core'; - - +import {Injectable} from '@angular/core'; function save() { - return function(target: Object, propertyKey: string) { + return function (target: Object, propertyKey: string) { const localStorageKey = `SaveDecorator.${target.constructor.name}.${propertyKey}`; const storedValue = localStorage.getItem(localStorageKey); let value: any = JSON.parse(storedValue ?? 'null'); @@ -11,9 +9,9 @@ function save() { let isInitialized = false; const setter = (newVal: any) => { - if(!wasStored || isInitialized) { + if (!wasStored || isInitialized) { value = newVal; - if(isInitialized) { + if (isInitialized) { localStorage.setItem(localStorageKey, JSON.stringify(value)); } } @@ -22,15 +20,13 @@ function save() { Object.defineProperty(target, propertyKey, { get: () => value, - set: setter - }); - } + set: setter, + }); + }; } - - @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ConfigService { @save() viewPortX = 0; @@ -47,14 +43,13 @@ export class ConfigService { @save() decimalPrecision = 3; } - @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ExportConfigService { @save() fill = true; @save() fillColor = '#000000'; @save() stroke = false; - @save() strokeColor = '#FF0000'; + @save() strokeColor = '#FF0000'; @save() strokeWidth = 0.1; -} \ No newline at end of file +} diff --git a/src/app/expandable/expandable.component.html b/src/app/expandable/expandable.component.html index 89d5d383..c1f7a964 100644 --- a/src/app/expandable/expandable.component.html +++ b/src/app/expandable/expandable.component.html @@ -1,10 +1,10 @@ -
- {{panelTitle}} -
{{panelInfo}}
- {{opened ? 'expand_more':'expand_less'}} +
+ {{ panelTitle }} +
{{ panelInfo }}
+ {{ opened ? 'expand_more' : 'expand_less' }} +
+
+
+ +
-
-
- -
-
\ No newline at end of file diff --git a/src/app/expandable/expandable.component.scss b/src/app/expandable/expandable.component.scss index 9f8667d7..165d2e8c 100644 --- a/src/app/expandable/expandable.component.scss +++ b/src/app/expandable/expandable.component.scss @@ -1,32 +1,32 @@ :host { - margin-bottom: 8px; - display: block; + margin-bottom: 8px; + display: block; } div.title { - color:white; - cursor: pointer; - background-color: #383838; - margin: 0; - padding: 2px 8px; - font-size:14px; - font-weight: normal; - text-transform: uppercase; - display: flex; - align-items: center; - user-select: none; - -webkit-user-select: none; - span { - flex-grow: 1; - } - .panel-info { - color:#C8C8C8; - font-style: italic; - font-size: 0.9em; - padding-right: 4px; - } + color: white; + cursor: pointer; + background-color: #383838; + margin: 0; + padding: 2px 8px; + font-size: 14px; + font-weight: normal; + text-transform: uppercase; + display: flex; + align-items: center; + user-select: none; + -webkit-user-select: none; + span { + flex-grow: 1; + } + .panel-info { + color: #c8c8c8; + font-style: italic; + font-size: 0.9em; + padding-right: 4px; + } } .panel { - transition: max-height 200ms; - overflow:hidden; -} \ No newline at end of file + transition: max-height 200ms; + overflow: hidden; +} diff --git a/src/app/expandable/expandable.component.spec.ts b/src/app/expandable/expandable.component.spec.ts index 2f672e7e..a7bdeb13 100644 --- a/src/app/expandable/expandable.component.spec.ts +++ b/src/app/expandable/expandable.component.spec.ts @@ -1,20 +1,21 @@ -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatIconModule } from '@angular/material/icon'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import {waitForAsync, ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatIconModule} from '@angular/material/icon'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; -import { ExpandableComponent } from './expandable.component'; +import {ExpandableComponent} from './expandable.component'; describe('ExpandableComponent', () => { let component: ExpandableComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [ ExpandableComponent ], - imports: [ BrowserAnimationsModule, MatIconModule ] + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ExpandableComponent], + imports: [BrowserAnimationsModule, MatIconModule], + }).compileComponents(); }) - .compileComponents(); - })); + ); beforeEach(() => { fixture = TestBed.createComponent(ExpandableComponent); diff --git a/src/app/expandable/expandable.component.ts b/src/app/expandable/expandable.component.ts index 4d3f65fb..fac95f70 100644 --- a/src/app/expandable/expandable.component.ts +++ b/src/app/expandable/expandable.component.ts @@ -1,5 +1,5 @@ -import { Component, Input } from '@angular/core'; -import { trigger, state, style, animate, transition } from '@angular/animations'; +import {Component, Input} from '@angular/core'; +import {trigger, state, style, animate, transition} from '@angular/animations'; @Component({ selector: 'app-expandable', @@ -10,18 +10,17 @@ import { trigger, state, style, animate, transition } from '@angular/animations' state('*', style({height: '*'})), transition(':enter', [style({height: '0'}), animate('100ms ease')]), transition(':leave', [animate('100ms ease', style({height: '0'}))]), - ]) - ] + ]), + ], }) export class ExpandableComponent { @Input() opened: boolean = true; @Input() panelTitle: string = ''; @Input() panelInfo: string = ''; - constructor() { } + constructor() {} toggle() { this.opened = !this.opened; } - } diff --git a/src/app/export/export-dialog.component.html b/src/app/export/export-dialog.component.html index 81897963..82571bdc 100644 --- a/src/app/export/export-dialog.component.html +++ b/src/app/export/export-dialog.component.html @@ -2,88 +2,149 @@

Export as SVG

-
- Style -
+
Style
- Fill + + Fill +
Fill Color - +
- Stroke + + Stroke +
Stroke Color - +
Stroke width - +
ViewBox
- +
X - +
Y - +
Width - +
Height - +
- - + - + - - + +
- -
\ No newline at end of file +
diff --git a/src/app/export/export-dialog.component.scss b/src/app/export/export-dialog.component.scss index 35f0eeda..bacf1b35 100644 --- a/src/app/export/export-dialog.component.scss +++ b/src/app/export/export-dialog.component.scss @@ -1,18 +1,18 @@ mat-form-field { - width:100%; + width: 100%; } .export-content { - display:flex; + display: flex; align-items: flex-start; flex-wrap: wrap; .export-form { - flex:1; - display:flex; + flex: 1; + display: flex; flex-wrap: wrap; - justify-content:space-between; + justify-content: space-between; .style-form-title { width: 100%; - margin-bottom:8px; + margin-bottom: 8px; } .style-form-fill { width: calc(33% - 4px); @@ -22,7 +22,7 @@ mat-form-field { .style-form-stroke-fields { display: flex; flex-wrap: wrap; - justify-content:space-between; + justify-content: space-between; .style-form-stroke-field { width: calc(50% - 4px); } @@ -30,26 +30,25 @@ mat-form-field { } .viewbox-form-title { width: 100%; - margin:8px 0; - display:flex; + margin: 8px 0; + display: flex; align-items: center; div { - flex:1; + flex: 1; } } .viewbox-form-field { - width:calc( 25% - 6px); + width: calc(25% - 6px); } } .preview { - width:324px; + width: 324px; svg { - margin:0 0 0 24px; + margin: 0 0 0 24px; } } } - @media screen and (max-width: 850px) { .style-form-fill { width: 100% !important; @@ -58,13 +57,13 @@ mat-form-field { width: 100% !important; } .viewbox-form-field { - width:calc( 50% - 4px) !important; + width: calc(50% - 4px) !important; } } @media screen and (max-width: 700px) { .preview { - width:100% !important; + width: 100% !important; svg { width: 100% !important; margin: 0 !important; diff --git a/src/app/export/export.component.html b/src/app/export/export.component.html index a79e8293..24e0ec2d 100644 --- a/src/app/export/export.component.html +++ b/src/app/export/export.component.html @@ -1,9 +1,10 @@ - diff --git a/src/app/export/export.component.spec.ts b/src/app/export/export.component.spec.ts index f858417a..884555e9 100644 --- a/src/app/export/export.component.spec.ts +++ b/src/app/export/export.component.spec.ts @@ -1,8 +1,8 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatIconModule } from '@angular/material/icon'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatIconModule} from '@angular/material/icon'; -import { ExportComponent } from './export.component'; +import {ExportComponent} from './export.component'; describe('ExportComponent', () => { let component: ExportComponent; @@ -10,10 +10,9 @@ describe('ExportComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ MatDialogModule, MatIconModule ], - declarations: [ ExportComponent ] - }) - .compileComponents(); + imports: [MatDialogModule, MatIconModule], + declarations: [ExportComponent], + }).compileComponents(); }); beforeEach(() => { diff --git a/src/app/export/export.component.ts b/src/app/export/export.component.ts index cf82191b..56c8c9f5 100644 --- a/src/app/export/export.component.ts +++ b/src/app/export/export.component.ts @@ -1,8 +1,12 @@ -import { Component, Inject, Input } from '@angular/core'; -import { MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; -import { ExportConfigService } from '../config.service'; -import { StorageService } from '../storage.service'; -import { Svg } from '../svg'; +import {Component, Inject, Input} from '@angular/core'; +import { + MatDialog, + MatDialogRef, + MAT_DIALOG_DATA, +} from '@angular/material/dialog'; +import {ExportConfigService} from '../config.service'; +import {StorageService} from '../storage.service'; +import {Svg} from '../svg'; interface DialogData { path: string; @@ -12,7 +16,7 @@ interface DialogData { @Component({ selector: 'app-export-dialog', templateUrl: 'export-dialog.component.html', - styleUrls: ['./export-dialog.component.scss'] + styleUrls: ['./export-dialog.component.scss'], }) export class ExportDialogComponent { x = 0; @@ -30,7 +34,7 @@ export class ExportDialogComponent { } download(fileName: string, data: string) { - const blob = new Blob([data], { type: 'image/svg+xml' }); + const blob = new Blob([data], {type: 'image/svg+xml'}); const anchor = document.createElement('a'); anchor.href = window.URL.createObjectURL(blob); anchor.setAttribute('download', fileName); @@ -45,16 +49,24 @@ export class ExportDialogComponent { this.dialogRef.close(); } onExport(): void { - const svg = -` - + const svg = ` + `; - this.download(this.data.name || 'svg-path.svg', svg); + this.download(this.data.name || 'svg-path.svg', svg); this.dialogRef.close(); } patternScale(containterWidth: number, containerHeight: number): number { - return Math.max(this.width / containterWidth, this.height / containerHeight); + return Math.max( + this.width / containterWidth, + this.height / containerHeight + ); } refreshViewbox() { @@ -63,8 +75,10 @@ export class ExportDialogComponent { if (locs.length > 0) { this.x = locs.reduce((acc, pt) => Math.min(acc, pt.x), Infinity); this.y = locs.reduce((acc, pt) => Math.min(acc, pt.y), Infinity); - this.width = locs.reduce((acc, pt) => Math.max(acc, pt.x), -Infinity) - this.x; - this.height = locs.reduce((acc, pt) => Math.max(acc, pt.y), -Infinity) - this.y; + this.width = + locs.reduce((acc, pt) => Math.max(acc, pt.x), -Infinity) - this.x; + this.height = + locs.reduce((acc, pt) => Math.max(acc, pt.y), -Infinity) - this.y; if (this.cfg.stroke) { this.x -= this.cfg.strokeWidth; this.y -= this.cfg.strokeWidth; @@ -75,25 +89,21 @@ export class ExportDialogComponent { } } - - @Component({ selector: 'app-export', - templateUrl: './export.component.html' + templateUrl: './export.component.html', }) export class ExportComponent { @Input() path: string = ''; @Input() name: string = ''; - constructor( - public dialog: MatDialog - ) {} + constructor(public dialog: MatDialog) {} openDialog(): void { const dialogRef = this.dialog.open(ExportDialogComponent, { width: '800px', panelClass: 'dialog', - data: {path: this.path, name: this.name} + data: {path: this.path, name: this.name}, }); } } diff --git a/src/app/formatter/formatter.directive.spec.ts b/src/app/formatter/formatter.directive.spec.ts index 484adcfc..89702fbc 100644 --- a/src/app/formatter/formatter.directive.spec.ts +++ b/src/app/formatter/formatter.directive.spec.ts @@ -1,5 +1,5 @@ -import { ElementRef } from '@angular/core'; -import { FormatterDirective } from './formatter.directive'; +import {ElementRef} from '@angular/core'; +import {FormatterDirective} from './formatter.directive'; describe('FormatterDirective', () => { it('should create an instance', () => { diff --git a/src/app/formatter/formatter.directive.ts b/src/app/formatter/formatter.directive.ts index 89c73fe8..18da07d8 100644 --- a/src/app/formatter/formatter.directive.ts +++ b/src/app/formatter/formatter.directive.ts @@ -1,16 +1,29 @@ -import { Directive, ElementRef, EventEmitter, HostListener, Input, OnChanges, Output, SimpleChanges } from '@angular/core'; -import { formatNumber } from '../svg'; +import { + Directive, + ElementRef, + EventEmitter, + HostListener, + Input, + OnChanges, + Output, + SimpleChanges, +} from '@angular/core'; +import {formatNumber} from '../svg'; @Directive({ - selector: '[appFormatter]' + selector: '[appFormatter]', }) export class FormatterDirective implements OnChanges { - @Input() formatterType: 'float'|'positive-float'|'integer'|'positive-integer' = 'float'; + @Input() formatterType: + | 'float' + | 'positive-float' + | 'integer' + | 'positive-integer' = 'float'; @Input() value: number = 0; @Output() valueChange = new EventEmitter(); internalValue: number = 0; - constructor(private el: ElementRef) { } + constructor(private el: ElementRef) {} private get viewValue(): string { return this.el.nativeElement.value; @@ -32,10 +45,22 @@ export class FormatterDirective implements OnChanges { @HostListener('input', ['$event']) onInput(e: InputEvent) { let value = ''; - if (this.formatterType === 'float') { value = this.viewValue.replace(/[\u066B,]/g, '.').replace(/[^\-0-9.eE]/g, ''); } - if (this.formatterType === 'integer') { value = this.viewValue.replace(/[^\-0-9]/g, ''); } - if (this.formatterType === 'positive-float') { value = this.viewValue.replace(/[\u066B,]/g, '.').replace(/[^0-9.eE]/g, ''); } - if (this.formatterType === 'positive-integer') { value = this.viewValue.replace(/[^0-9]/g, ''); } + if (this.formatterType === 'float') { + value = this.viewValue + .replace(/[\u066B,]/g, '.') + .replace(/[^\-0-9.eE]/g, ''); + } + if (this.formatterType === 'integer') { + value = this.viewValue.replace(/[^\-0-9]/g, ''); + } + if (this.formatterType === 'positive-float') { + value = this.viewValue + .replace(/[\u066B,]/g, '.') + .replace(/[^0-9.eE]/g, ''); + } + if (this.formatterType === 'positive-integer') { + value = this.viewValue.replace(/[^0-9]/g, ''); + } this.viewValue = value; const floatValue = parseFloat(value); diff --git a/src/app/image.ts b/src/app/image.ts index 438018f1..30ea9961 100644 --- a/src/app/image.ts +++ b/src/app/image.ts @@ -1,9 +1,9 @@ export interface Image { - x1: number; - y1: number; - x2: number; - y2: number; - preserveAspectRatio: boolean; - opacity: number; - data: string; + x1: number; + y1: number; + x2: number; + y2: number; + preserveAspectRatio: boolean; + opacity: number; + data: string; } diff --git a/src/app/keyboard-navigable/keyboard-navigable.directive.spec.ts b/src/app/keyboard-navigable/keyboard-navigable.directive.spec.ts index 5cb29055..5a49004c 100644 --- a/src/app/keyboard-navigable/keyboard-navigable.directive.spec.ts +++ b/src/app/keyboard-navigable/keyboard-navigable.directive.spec.ts @@ -1,7 +1,5 @@ -import { ElementRef } from '@angular/core'; -import { KeyboardNavigableDirective } from './keyboard-navigable.directive'; - - +import {ElementRef} from '@angular/core'; +import {KeyboardNavigableDirective} from './keyboard-navigable.directive'; describe('KeyboardNavigableDirective', () => { it('should create an instance', () => { diff --git a/src/app/keyboard-navigable/keyboard-navigable.directive.ts b/src/app/keyboard-navigable/keyboard-navigable.directive.ts index f4e325c1..f9901afc 100644 --- a/src/app/keyboard-navigable/keyboard-navigable.directive.ts +++ b/src/app/keyboard-navigable/keyboard-navigable.directive.ts @@ -1,15 +1,17 @@ -import { Directive, ElementRef, HostListener, Input } from '@angular/core'; +import {Directive, ElementRef, HostListener, Input} from '@angular/core'; @Directive({ - selector: '[appKeyboardNavigable]' + selector: '[appKeyboardNavigable]', }) export class KeyboardNavigableDirective { @Input() keyboardNavigableIdPrefix: string = ''; - constructor(private el: ElementRef) { } + constructor(private el: ElementRef) {} setFocus(row: number, col: number, event: KeyboardEvent): boolean { - const el = document.getElementById(`${this.keyboardNavigableIdPrefix}_${row}_${col}`) as HTMLInputElement; + const el = document.getElementById( + `${this.keyboardNavigableIdPrefix}_${row}_${col}` + ) as HTMLInputElement; if (el) { el.focus(); el.select(); @@ -19,7 +21,7 @@ export class KeyboardNavigableDirective { return false; } - moveFocusTo(type: 'left'|'right'|'up'|'down', event: KeyboardEvent) { + moveFocusTo(type: 'left' | 'right' | 'up' | 'down', event: KeyboardEvent) { const id = this.el.nativeElement.getAttribute('id') || ''; let row = parseInt(id.replace(/.*_([0-9]+)_[0-9]+$/, '$1'), 10); let col = parseInt(id.replace(/.*_[0-9]+_([0-9]+)$/, '$1'), 10); @@ -42,7 +44,10 @@ export class KeyboardNavigableDirective { return; } if (type === 'right') { - if (this.setFocus(row, col + 1, event) || this.setFocus(row + 1, 0, event)) { + if ( + this.setFocus(row, col + 1, event) || + this.setFocus(row + 1, 0, event) + ) { return; } } @@ -52,7 +57,7 @@ export class KeyboardNavigableDirective { } let col2 = 7; while (col2 >= 0) { - col2 --; + col2--; if (this.setFocus(row - 1, col2, event)) { return; } @@ -60,16 +65,16 @@ export class KeyboardNavigableDirective { } let col3 = col; while ((type === 'down' || type === 'up') && col3 > 0) { - col3 --; + col3--; if (this.setFocus(row, col3, event)) { return; } } - count ++; + count++; if (type === 'up' || type === 'left') { - row --; + row--; } else { - row ++; + row++; } } } diff --git a/src/app/open/open-dialog.component.css b/src/app/open/open-dialog.component.css index 4486b81d..8452e885 100644 --- a/src/app/open/open-dialog.component.css +++ b/src/app/open/open-dialog.component.css @@ -1,16 +1,18 @@ table { - margin: 0 -24px; - width: calc( 100% + 48px); + margin: 0 -24px; + width: calc(100% + 48px); } .mat-sort-header { - user-select: none; + user-select: none; } .mat-row { - cursor: pointer; + cursor: pointer; } -th.mat-header-cell:first-of-type, td.mat-cell:first-of-type, td.mat-footer-cell:first-of-type { - padding-left: 8px; -} \ No newline at end of file +th.mat-header-cell:first-of-type, +td.mat-cell:first-of-type, +td.mat-footer-cell:first-of-type { + padding-left: 8px; +} diff --git a/src/app/open/open-dialog.component.html b/src/app/open/open-dialog.component.html index 9799302c..633964b7 100644 --- a/src/app/open/open-dialog.component.html +++ b/src/app/open/open-dialog.component.html @@ -2,23 +2,43 @@

Open path

- - + + - - + + - - + + - + - - - - - + + +
Name {{element.name}} Name{{ element.name }} Creation Date {{formatDate(element.creationDate)}} + Creation Date + + {{ formatDate(element.creationDate) }} + Change Date {{formatDate(element.changeDate)}} + Change Date + + {{ formatDate(element.changeDate) }} + - @@ -26,24 +46,35 @@

Open path

- + - - -
-
\ No newline at end of file +
diff --git a/src/app/open/open.component.html b/src/app/open/open.component.html index 7e164208..38f6bccc 100644 --- a/src/app/open/open.component.html +++ b/src/app/open/open.component.html @@ -1,11 +1,12 @@ - \ No newline at end of file + folder_open + diff --git a/src/app/open/open.component.spec.ts b/src/app/open/open.component.spec.ts index dab4ab5a..adec7342 100644 --- a/src/app/open/open.component.spec.ts +++ b/src/app/open/open.component.spec.ts @@ -1,8 +1,8 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatIconModule } from '@angular/material/icon'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatIconModule} from '@angular/material/icon'; -import { OpenComponent } from './open.component'; +import {OpenComponent} from './open.component'; describe('OpenComponent', () => { let component: OpenComponent; @@ -10,10 +10,9 @@ describe('OpenComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ MatDialogModule, MatIconModule ], - declarations: [ OpenComponent ] - }) - .compileComponents(); + imports: [MatDialogModule, MatIconModule], + declarations: [OpenComponent], + }).compileComponents(); }); beforeEach(() => { diff --git a/src/app/open/open.component.ts b/src/app/open/open.component.ts index cb4fdff6..d52f249c 100644 --- a/src/app/open/open.component.ts +++ b/src/app/open/open.component.ts @@ -1,9 +1,20 @@ -import { Component, Inject, Output, EventEmitter, ViewChild, AfterViewInit } from '@angular/core'; -import { MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; -import { MatSort } from '@angular/material/sort'; -import { MatTableDataSource } from '@angular/material/table'; -import { StorageService } from '../storage.service'; -import { Svg } from '../svg'; +import { + Component, + Inject, + Output, + EventEmitter, + ViewChild, + AfterViewInit, +} from '@angular/core'; +import { + MatDialog, + MatDialogRef, + MAT_DIALOG_DATA, +} from '@angular/material/dialog'; +import {MatSort} from '@angular/material/sort'; +import {MatTableDataSource} from '@angular/material/table'; +import {StorageService} from '../storage.service'; +import {Svg} from '../svg'; export class DialogData { name?: string; @@ -12,17 +23,18 @@ export class DialogData { @Component({ selector: 'app-open-dialog', templateUrl: 'open-dialog.component.html', - styleUrls: ['./open-dialog.component.css'] + styleUrls: ['./open-dialog.component.css'], }) export class OpenDialogComponent implements AfterViewInit { constructor( public dialogRef: MatDialogRef, public storageService: StorageService, @Inject(MAT_DIALOG_DATA) public data: DialogData - ) { - } + ) {} - dataSource = new MatTableDataSource(this.storageService.storedPaths.filter(it => !!it.name)); + dataSource = new MatTableDataSource( + this.storageService.storedPaths.filter((it) => !!it.name) + ); displayedColumns = ['preview', 'name', 'create', 'change', 'actions']; beingRemoved?: string; @ViewChild(MatSort) sort: MatSort | null = null; @@ -31,18 +43,26 @@ export class OpenDialogComponent implements AfterViewInit { this.dataSource.sort = this.sort; this.dataSource.sortingDataAccessor = (item, property) => { switch (property) { - case 'change': return new Date(item.changeDate).getTime(); - case 'create': return new Date(item.creationDate).getTime(); - case 'name': return item.name || ''; - default: return item.path; + case 'change': + return new Date(item.changeDate).getTime(); + case 'create': + return new Date(item.creationDate).getTime(); + case 'name': + return item.name || ''; + default: + return item.path; } }; } formatDate(date: Date): string { const options: Intl.DateTimeFormatOptions = { - year: 'numeric', month: 'numeric', day: 'numeric', - hour: 'numeric', minute: 'numeric', second: 'numeric' + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', }; return new Intl.DateTimeFormat(undefined, options).format(date); } @@ -67,7 +87,9 @@ export class OpenDialogComponent implements AfterViewInit { } onRemove(name: string): void { this.storageService.removePath(name); - this.dataSource = new MatTableDataSource(this.storageService.storedPaths.filter(it => !!it.name)); + this.dataSource = new MatTableDataSource( + this.storageService.storedPaths.filter((it) => !!it.name) + ); this.beingRemoved = undefined; } } @@ -75,31 +97,28 @@ export class OpenDialogComponent implements AfterViewInit { @Component({ selector: 'app-open', templateUrl: './open.component.html', - styleUrls: ['./open.component.css'] + styleUrls: ['./open.component.css'], }) export class OpenComponent { - @Output() openPath = new EventEmitter<{name: string, path: string}>(); + @Output() openPath = new EventEmitter<{name: string; path: string}>(); constructor( public dialog: MatDialog, - public storageService: StorageService, + public storageService: StorageService ) {} openDialog(): void { const dialogRef = this.dialog.open(OpenDialogComponent, { width: '800px', panelClass: 'dialog', - autoFocus: false + autoFocus: false, }); - dialogRef.afterClosed().subscribe((result: DialogData) => { + dialogRef.afterClosed().subscribe((result: DialogData) => { if (result) { const storedPath = this.storageService.getPath(result.name); - if(storedPath && storedPath.name && storedPath.path) { - this.openPath.emit({ - name: storedPath.name, - path: storedPath.path - }); + if (storedPath && storedPath.name && storedPath.path) { + this.openPath.emit({name: storedPath.name, path: storedPath.path}); } } }); diff --git a/src/app/save/save-dialog.component.html b/src/app/save/save-dialog.component.html index 78f08482..736fa071 100644 --- a/src/app/save/save-dialog.component.html +++ b/src/app/save/save-dialog.component.html @@ -1,20 +1,31 @@

Save path

- + Name - +

- A path with this name already exists, press Replace to replace it. + A path with this name already exists, press + Replace + to replace it.

- Press Save to save current path in your browser. + Press + Save + to save current path in your browser.

- -
\ No newline at end of file + diff --git a/src/app/save/save.component.html b/src/app/save/save.component.html index c881567c..d1cc126f 100644 --- a/src/app/save/save.component.html +++ b/src/app/save/save.component.html @@ -1,10 +1,11 @@ - \ No newline at end of file + save + diff --git a/src/app/save/save.component.spec.ts b/src/app/save/save.component.spec.ts index 1cc929fb..0e88374d 100644 --- a/src/app/save/save.component.spec.ts +++ b/src/app/save/save.component.spec.ts @@ -1,8 +1,8 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatIconModule } from '@angular/material/icon'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatIconModule} from '@angular/material/icon'; -import { SaveComponent } from './save.component'; +import {SaveComponent} from './save.component'; describe('SaveComponent', () => { let component: SaveComponent; @@ -10,10 +10,9 @@ describe('SaveComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ MatDialogModule, MatIconModule ], - declarations: [ SaveComponent ] - }) - .compileComponents(); + imports: [MatDialogModule, MatIconModule], + declarations: [SaveComponent], + }).compileComponents(); }); beforeEach(() => { diff --git a/src/app/save/save.component.ts b/src/app/save/save.component.ts index f220a1df..9d8778e4 100644 --- a/src/app/save/save.component.ts +++ b/src/app/save/save.component.ts @@ -1,6 +1,10 @@ -import { Component, Inject, Input, Output, EventEmitter } from '@angular/core'; -import { MatDialog, MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog'; -import { StorageService } from '../storage.service'; +import {Component, Inject, Input, Output, EventEmitter} from '@angular/core'; +import { + MatDialog, + MatDialogRef, + MAT_DIALOG_DATA, +} from '@angular/material/dialog'; +import {StorageService} from '../storage.service'; export interface DialogData { name: string; @@ -15,8 +19,7 @@ export class SaveDialogComponent { public dialogRef: MatDialogRef, public storageService: StorageService, @Inject(MAT_DIALOG_DATA) public data: DialogData - ) { - } + ) {} onCancel(): void { this.dialogRef.close(); } @@ -28,7 +31,7 @@ export class SaveDialogComponent { @Component({ selector: 'app-save', templateUrl: './save.component.html', - styleUrls: ['./save.component.css'] + styleUrls: ['./save.component.css'], }) export class SaveComponent { @Input() path: string = ''; @@ -37,7 +40,7 @@ export class SaveComponent { constructor( public dialog: MatDialog, - public storageService: StorageService, + public storageService: StorageService ) {} openDialog(): void { @@ -54,10 +57,10 @@ export class SaveComponent { const dialogRef = this.dialog.open(SaveDialogComponent, { width: '300px', panelClass: 'dialog', - data: {name} + data: {name}, }); - dialogRef.afterClosed().subscribe((result: DialogData) => { + dialogRef.afterClosed().subscribe((result: DialogData) => { if (result) { this.storageService.addPath(result.name, this.path); this.name = result.name; diff --git a/src/app/storage.service.spec.ts b/src/app/storage.service.spec.ts index e7fe5b53..d19e6d1a 100644 --- a/src/app/storage.service.spec.ts +++ b/src/app/storage.service.spec.ts @@ -1,6 +1,6 @@ -import { TestBed } from '@angular/core/testing'; +import {TestBed} from '@angular/core/testing'; -import { StorageService } from './storage.service'; +import {StorageService} from './storage.service'; describe('StorageService', () => { let service: StorageService; diff --git a/src/app/storage.service.ts b/src/app/storage.service.ts index d10abb34..e7887a31 100644 --- a/src/app/storage.service.ts +++ b/src/app/storage.service.ts @@ -1,14 +1,14 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; export class StoredPath { - name: string | null = ''; + name: string | null = ''; path = ''; creationDate: Date = new Date(); changeDate: Date = new Date(); } @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class StorageService { storedPaths: StoredPath[] = []; @@ -20,12 +20,12 @@ export class StorageService { return this.getPath(name) !== undefined; } - getPath(name: string | null = null): StoredPath | undefined { - return this.storedPaths.find(it => it.name === name); + getPath(name: string | null = null): StoredPath | undefined { + return this.storedPaths.find((it) => it.name === name); } removePath(name: string) { - this.storedPaths = this.storedPaths.filter(it => it.name !== name); + this.storedPaths = this.storedPaths.filter((it) => it.name !== name); this.save(); } @@ -42,7 +42,7 @@ export class StorageService { } isEmpty(): boolean { - return this.storedPaths.filter(it => !!it.name).length === 0; + return this.storedPaths.filter((it) => !!it.name).length === 0; } load() { @@ -50,7 +50,7 @@ export class StorageService { const stored = localStorage.getItem('storedPaths'); if (stored) { const parsed = JSON.parse(stored) as any[]; - parsed.forEach(it => { + parsed.forEach((it) => { it.creationDate = new Date(it.creationDate); it.changeDate = new Date(it.changeDate); }); diff --git a/src/app/svg-parser.spec.ts b/src/app/svg-parser.spec.ts index 650a960a..8ba951fd 100644 --- a/src/app/svg-parser.spec.ts +++ b/src/app/svg-parser.spec.ts @@ -1,4 +1,4 @@ -import { SvgParser } from './svg-parser'; +import {SvgParser} from './svg-parser'; const parse = SvgParser.parse; @@ -21,7 +21,7 @@ it('overloaded moveTo', () => { ['l', '39', '0'], ['l', '0', '-40'], ['l', '-39', '0'], - ['z'] + ['z'], ]); }); @@ -30,17 +30,19 @@ it('curveTo', () => { const b = parse('c 50,0 50,100 100,100 c 50,0 50,-100 100,-100'); expect(a).toEqual([ ['c', '50', '0', '50', '100', '100', '100'], - ['c', '50', '0', '50', '-100', '100', '-100'] + ['c', '50', '0', '50', '-100', '100', '-100'], ]); expect(a).toEqual(b); }); it('lineTo', () => { - expect(() => { parse('l 10 10 0'); }).toThrowError(/malformed/); + expect(() => { + parse('l 10 10 0'); + }).toThrowError(/malformed/); expect(parse('l 10,10')).toEqual([['l', '10', '10']]); expect(parse('l10 10 10 10')).toEqual([ ['l', '10', '10'], - ['l', '10', '10'] + ['l', '10', '10'], ]); }); @@ -54,17 +56,17 @@ it('verticalTo', () => { it('arcTo', () => { expect(parse('A 30 50 0 0 1 162.55 162.45')).toEqual([ - ['A', '30', '50', '0', '0', '1', '162.55', '162.45'] + ['A', '30', '50', '0', '0', '1', '162.55', '162.45'], ]); expect(parse('A 60 60 0 01100 100')).toEqual([ - ['A', '60', '60', '0', '0', '1', '100', '100'] + ['A', '60', '60', '0', '0', '1', '100', '100'], ]); }); it('quadratic curveTo', () => { expect(parse('M10 80 Q 95 10 180 80')).toEqual([ ['M', '10', '80'], - ['Q', '95', '10', '180', '80'] + ['Q', '95', '10', '180', '80'], ]); }); diff --git a/src/app/svg-parser.ts b/src/app/svg-parser.ts index 5f8db4f4..c4e3fe88 100644 --- a/src/app/svg-parser.ts +++ b/src/app/svg-parser.ts @@ -1,76 +1,94 @@ const kCommandTypeRegex = /^[\t\n\f\r ]*([MLHVZCSQTAmlhvzcsqta])[\t\n\f\r ]*/; const kFlagRegex = /^[01]/; -const kNumberRegex = /^[+-]?(([0-9]*\.[0-9]+)|([0-9]+\.)|([0-9]+))([eE][+-]?[0-9]+)?/; +const kNumberRegex = + /^[+-]?(([0-9]*\.[0-9]+)|([0-9]+\.)|([0-9]+))([eE][+-]?[0-9]+)?/; const kCoordinateRegex = kNumberRegex; const kCommaWsp = /^(([\t\n\f\r ]+,?[\t\n\f\r ]*)|(,[\t\n\f\r ]*))/; -const kGrammar: {[key: string]: RegExp[]} = { - M: [kCoordinateRegex, kCoordinateRegex], - L: [kCoordinateRegex, kCoordinateRegex], - H: [kCoordinateRegex], - V: [kCoordinateRegex], - Z: [], - C: [kCoordinateRegex, kCoordinateRegex, kCoordinateRegex, kCoordinateRegex, kCoordinateRegex, kCoordinateRegex], - S: [kCoordinateRegex, kCoordinateRegex, kCoordinateRegex, kCoordinateRegex], - Q: [kCoordinateRegex, kCoordinateRegex, kCoordinateRegex, kCoordinateRegex], - T: [kCoordinateRegex, kCoordinateRegex], - A: [kNumberRegex, kNumberRegex, kCoordinateRegex, kFlagRegex, kFlagRegex, kCoordinateRegex, kCoordinateRegex], +const kGrammar: {[key: string]: RegExp[]} = { + M: [kCoordinateRegex, kCoordinateRegex], + L: [kCoordinateRegex, kCoordinateRegex], + H: [kCoordinateRegex], + V: [kCoordinateRegex], + Z: [], + C: [ + kCoordinateRegex, + kCoordinateRegex, + kCoordinateRegex, + kCoordinateRegex, + kCoordinateRegex, + kCoordinateRegex, + ], + S: [kCoordinateRegex, kCoordinateRegex, kCoordinateRegex, kCoordinateRegex], + Q: [kCoordinateRegex, kCoordinateRegex, kCoordinateRegex, kCoordinateRegex], + T: [kCoordinateRegex, kCoordinateRegex], + A: [ + kNumberRegex, + kNumberRegex, + kCoordinateRegex, + kFlagRegex, + kFlagRegex, + kCoordinateRegex, + kCoordinateRegex, + ], }; export class SvgParser { + static components( + type: string, + path: string, + cursor: number + ): [number, string[][]] { + const expectedRegexList = kGrammar[type.toUpperCase()]; - static components(type: string, path: string, cursor: number): [number, string[][]] - { - const expectedRegexList = kGrammar[type.toUpperCase()]; + const components: string[][] = []; + while (cursor <= path.length) { + const component: string[] = [type]; + for (const regex of expectedRegexList) { + const match = path.slice(cursor).match(regex); - const components: string[][] = []; - while (cursor <= path.length) { - const component: string[] = [type]; - for (const regex of expectedRegexList) { - const match = path.slice(cursor).match(regex); - - if (match !== null) { - component.push(match[0]); - cursor += match[0].length; - const ws = path.slice(cursor).match(kCommaWsp); - if (ws !== null) { - cursor += ws[0].length; - } - } else if (component.length === 1) { - return [cursor, components]; - } else { - throw new Error('malformed path (first error at ' + cursor + ')'); - } - } - components.push(component); - if (expectedRegexList.length === 0) { - return [cursor, components]; - } - if (type === 'm') { - type = 'l'; - } - if (type === 'M') { - type = 'L'; - } + if (match !== null) { + component.push(match[0]); + cursor += match[0].length; + const ws = path.slice(cursor).match(kCommaWsp); + if (ws !== null) { + cursor += ws[0].length; + } + } else if (component.length === 1) { + return [cursor, components]; + } else { + throw new Error('malformed path (first error at ' + cursor + ')'); } - throw new Error('malformed path (first error at ' + cursor + ')'); + } + components.push(component); + if (expectedRegexList.length === 0) { + return [cursor, components]; + } + if (type === 'm') { + type = 'l'; + } + if (type === 'M') { + type = 'L'; + } } + throw new Error('malformed path (first error at ' + cursor + ')'); + } - public static parse(path: string): string[][] { - let cursor = 0; - let tokens: string[][] = []; - while (cursor < path.length) { - const match = path.slice(cursor).match(kCommandTypeRegex); - if (match !== null) { - const command = match[1]; - cursor += match[0].length; - const componentList = SvgParser.components(command, path, cursor); - cursor = componentList[0]; - tokens = [...tokens, ...componentList[1]]; - } else { - throw new Error('malformed path (first error at ' + cursor + ')'); - } - } - return tokens; + public static parse(path: string): string[][] { + let cursor = 0; + let tokens: string[][] = []; + while (cursor < path.length) { + const match = path.slice(cursor).match(kCommandTypeRegex); + if (match !== null) { + const command = match[1]; + cursor += match[0].length; + const componentList = SvgParser.components(command, path, cursor); + cursor = componentList[0]; + tokens = [...tokens, ...componentList[1]]; + } else { + throw new Error('malformed path (first error at ' + cursor + ')'); + } } + return tokens; + } } diff --git a/src/app/svg.spec.ts b/src/app/svg.spec.ts index 2c1e213c..7d3fa334 100644 --- a/src/app/svg.spec.ts +++ b/src/app/svg.spec.ts @@ -1,13 +1,14 @@ -import { Svg } from './svg'; +import {Svg} from './svg'; +const swordPath = + `M 4 8 L 10 1 L 13 0 L 12 3 L 5 9 C 6 10 6 11 7 10 C 7 11 8 12 7 12 A 1.42 1.42 0 0 1 6 13 ` + + `A 5 5 0 0 0 4 10 Q 3.5 9.9 3.5 10.5 T 2 11.8 T 1.2 11 T 2.5 9.5 T 3 9 A 5 5 90 0 0 0 7 A 1.42 1.42 0 0 1 1 6 ` + + `C 1 5 2 6 3 6 C 2 7 3 7 4 8 M 10 1 L 10 3 L 12 3 L 10.2 2.8 L 10 1`; -const swordPath = `M 4 8 L 10 1 L 13 0 L 12 3 L 5 9 C 6 10 6 11 7 10 C 7 11 8 12 7 12 A 1.42 1.42 0 0 1 6 13 ` -+ `A 5 5 0 0 0 4 10 Q 3.5 9.9 3.5 10.5 T 2 11.8 T 1.2 11 T 2.5 9.5 T 3 9 A 5 5 90 0 0 0 7 A 1.42 1.42 0 0 1 1 6 ` -+ `C 1 5 2 6 3 6 C 2 7 3 7 4 8 M 10 1 L 10 3 L 12 3 L 10.2 2.8 L 10 1`; - -const minifiedSwordPath = `M4 8 10 1 13 0 12 3 5 9C6 10 6 11 7 10 7 11 8 12 7 12A1.42 1.42 0 016 13 5 5 0 004 10` -+ `Q3.5 9.9 3.5 10.5T2 11.8 1.2 11 2.5 9.5 3 9A5 5 90 000 7 1.42 1.42 0 011 6C1 5 2 6 3 6 2 7 3 7 4 8M10 1 10 3` -+ ` 12 3 10.2 2.8 10 1`; +const minifiedSwordPath = + `M4 8 10 1 13 0 12 3 5 9C6 10 6 11 7 10 7 11 8 12 7 12A1.42 1.42 0 016 13 5 5 0 004 10` + + `Q3.5 9.9 3.5 10.5T2 11.8 1.2 11 2.5 9.5 3 9A5 5 90 000 7 1.42 1.42 0 011 6C1 5 2 6 3 6 2 7 3 7 4 8M10 1 10 3` + + ` 12 3 10.2 2.8 10 1`; it('should decode and rencode the sword path', () => { const svg = new Svg(swordPath); @@ -20,21 +21,33 @@ it('should minify the sword path', () => { }); it('should merge lines', () => { - expect(new Svg(`m 0 0 l 1 1 l 1 2`).asString(3, true)).toEqual(`m0 0 1 1 1 2`); - expect(new Svg(`M 0 0 L 1 1 L 1 2`).asString(3, true)).toEqual(`M0 0 1 1 1 2`); - expect(new Svg(`m 0 0 L 1 1 l 1 2 l 3 3`).asString(3, true)).toEqual(`m0 0L1 1l1 2 3 3`); - expect(new Svg(`M 0 0 l 1 1 L 1 2 L 3 3`).asString(3, true)).toEqual(`M0 0l1 1L1 2 3 3`); - expect(new Svg(`m 0.1 0.2 l 0.6 0.5 l 0.3 0.4`).asString(3, true)).toEqual(`m.1.2.6.5.3.4`); + expect(new Svg(`m 0 0 l 1 1 l 1 2`).asString(3, true)).toEqual( + `m0 0 1 1 1 2` + ); + expect(new Svg(`M 0 0 L 1 1 L 1 2`).asString(3, true)).toEqual( + `M0 0 1 1 1 2` + ); + expect(new Svg(`m 0 0 L 1 1 l 1 2 l 3 3`).asString(3, true)).toEqual( + `m0 0L1 1l1 2 3 3` + ); + expect(new Svg(`M 0 0 l 1 1 L 1 2 L 3 3`).asString(3, true)).toEqual( + `M0 0l1 1L1 2 3 3` + ); + expect(new Svg(`m 0.1 0.2 l 0.6 0.5 l 0.3 0.4`).asString(3, true)).toEqual( + `m.1.2.6.5.3.4` + ); }); it('should merge consecutive commands', () => { - const path = `M -7 -7 L -6 -7 L -6 -6 V -5 V -4 H -5 H -4 ` - + `C -4 -5 -3 -4 -3 -5 C -4 -5 -3 -4 -3 -5 S -2 -7 -2 -6 S -1 -6 -1 -5 ` - + `Q -1 -4 -2 -4 Q -2 -3 -3 -3 T -4 -2 T -5 -1 ` - + `A 1 1 0 0 0 -6 -2.1 A 0.1 1 0 0 0 -7 -3 z`; - const minifiedPath = `M-7-7-6-7-6-6V-5-4H-5-4` - + `C-4-5-3-4-3-5-4-5-3-4-3-5S-2-7-2-6-1-6-1-5` - + `Q-1-4-2-4-2-3-3-3T-4-2-5-1` - + `A1 1 0 00-6-2.1.1 1 0 00-7-3z`; + const path = + `M -7 -7 L -6 -7 L -6 -6 V -5 V -4 H -5 H -4 ` + + `C -4 -5 -3 -4 -3 -5 C -4 -5 -3 -4 -3 -5 S -2 -7 -2 -6 S -1 -6 -1 -5 ` + + `Q -1 -4 -2 -4 Q -2 -3 -3 -3 T -4 -2 T -5 -1 ` + + `A 1 1 0 0 0 -6 -2.1 A 0.1 1 0 0 0 -7 -3 z`; + const minifiedPath = + `M-7-7-6-7-6-6V-5-4H-5-4` + + `C-4-5-3-4-3-5-4-5-3-4-3-5S-2-7-2-6-1-6-1-5` + + `Q-1-4-2-4-2-3-3-3T-4-2-5-1` + + `A1 1 0 00-6-2.1.1 1 0 00-7-3z`; expect(new Svg(path).asString(3, true)).toEqual(minifiedPath); -}); \ No newline at end of file +}); diff --git a/src/app/svg.ts b/src/app/svg.ts index b4bf963c..03f9f601 100644 --- a/src/app/svg.ts +++ b/src/app/svg.ts @@ -1,603 +1,703 @@ -import { SvgParser } from './svg-parser'; +import {SvgParser} from './svg-parser'; export function formatNumber(v: number, d: number, minify = false): string { - let result = v.toFixed(d) - .replace(/^(-?[0-9]*\.([0-9]*[1-9])?)0*$/, '$1') - .replace(/\.$/, ''); - if (minify) { - result = result.replace(/^(-?)0\./, '$1.'); - } - return result; + let result = v + .toFixed(d) + .replace(/^(-?[0-9]*\.([0-9]*[1-9])?)0*$/, '$1') + .replace(/\.$/, ''); + if (minify) { + result = result.replace(/^(-?)0\./, '$1.'); + } + return result; } export class Point { - constructor( - public x: number, - public y: number - ){} - } + constructor(public x: number, public y: number) {} +} export class SvgPoint extends Point { - itemReference: SvgItem = new DummySvgItem(); - movable = true; - constructor( - x: number, - y: number, - movable = true - ){ - super(x, y); - this.movable = movable; - } - } + itemReference: SvgItem = new DummySvgItem(); + movable = true; + constructor(x: number, y: number, movable = true) { + super(x, y); + this.movable = movable; + } +} export class SvgControlPoint extends SvgPoint { - subIndex: number = 0; - constructor( - point: Point, - public relations: Point[], - movable = true - ){ - super(point.x, point.y, movable); - } - } + subIndex: number = 0; + constructor(point: Point, public relations: Point[], movable = true) { + super(point.x, point.y, movable); + } +} export abstract class SvgItem { - - constructor(values: number[], relative: boolean) { - this.values = values; - this.relative = relative; + constructor(values: number[], relative: boolean) { + this.values = values; + this.relative = relative; + } + relative: boolean; + values: number[]; + previousPoint: Point = new Point(0, 0); + absolutePoints: SvgPoint[] = []; + absoluteControlPoints: SvgControlPoint[] = []; + + public static Make(rawItem: string[]): SvgItem { + let result: SvgItem | undefined = undefined; + const relative = rawItem[0].toUpperCase() !== rawItem[0]; + const values = rawItem.slice(1).map((it) => parseFloat(it)); + switch (rawItem[0].toUpperCase()) { + case MoveTo.key: + result = new MoveTo(values, relative); + break; + case LineTo.key: + result = new LineTo(values, relative); + break; + case HorizontalLineTo.key: + result = new HorizontalLineTo(values, relative); + break; + case VerticalLineTo.key: + result = new VerticalLineTo(values, relative); + break; + case ClosePath.key: + result = new ClosePath(values, relative); + break; + case CurveTo.key: + result = new CurveTo(values, relative); + break; + case SmoothCurveTo.key: + result = new SmoothCurveTo(values, relative); + break; + case QuadraticBezierCurveTo.key: + result = new QuadraticBezierCurveTo(values, relative); + break; + case SmoothQuadraticBezierCurveTo.key: + result = new SmoothQuadraticBezierCurveTo(values, relative); + break; + case EllipticalArcTo.key: + result = new EllipticalArcTo(values, relative); + break; + } + if (!result) { + throw 'Invalid SVG item'; } - relative: boolean; - values: number[]; - previousPoint: Point = new Point(0, 0); - absolutePoints: SvgPoint[] = []; - absoluteControlPoints: SvgControlPoint[] = []; - - public static Make(rawItem: string[]): SvgItem { - let result: SvgItem | undefined = undefined; - const relative = rawItem[0].toUpperCase() !== rawItem[0]; - const values = rawItem.slice(1).map( it => parseFloat(it) ); - switch (rawItem[0].toUpperCase()) { - case MoveTo.key: result = new MoveTo(values, relative); break; - case LineTo.key: result = new LineTo(values, relative); break; - case HorizontalLineTo.key: result = new HorizontalLineTo(values, relative); break; - case VerticalLineTo.key: result = new VerticalLineTo(values, relative); break; - case ClosePath.key: result = new ClosePath(values, relative); break; - case CurveTo.key: result = new CurveTo(values, relative); break; - case SmoothCurveTo.key: result = new SmoothCurveTo(values, relative); break; - case QuadraticBezierCurveTo.key: result = new QuadraticBezierCurveTo(values, relative); break; - case SmoothQuadraticBezierCurveTo.key: result = new SmoothQuadraticBezierCurveTo(values, relative); break; - case EllipticalArcTo.key: result = new EllipticalArcTo(values, relative); break; - } - if(!result) { - throw 'Invalid SVG item'; - } - return result; - } - - public static MakeFrom(origin: SvgItem, previous: SvgItem, newType: string) { - const target = origin.targetLocation(); - const x = target.x.toString(); - const y = target.y.toString(); - let values: string[] = []; - const absoluteType = newType.toUpperCase(); - switch (absoluteType) { - case MoveTo.key: values = [MoveTo.key, x, y]; break; - case LineTo.key: values = [LineTo.key, x, y]; break; - case HorizontalLineTo.key: values = [HorizontalLineTo.key, x]; break; - case VerticalLineTo.key: values = [VerticalLineTo.key, y]; break; - case ClosePath.key: values = [ClosePath.key]; break; - case CurveTo.key: values = [CurveTo.key, '0', '0', '0', '0', x, y]; break; - case SmoothCurveTo.key: values = [SmoothCurveTo.key, '0', '0', x, y]; break; - case QuadraticBezierCurveTo.key: values = [QuadraticBezierCurveTo.key, '0', '0', x, y]; break; - case SmoothQuadraticBezierCurveTo.key: values = [SmoothQuadraticBezierCurveTo.key, x, y]; break; - case EllipticalArcTo.key: values = [EllipticalArcTo.key, '1' , '1', '0', '0', '0', x, y]; break; - } - const result = SvgItem.Make(values); - - const controlPoints = origin.absoluteControlPoints; - - result.previousPoint = previous.targetLocation(); - result.absolutePoints = [target]; - result.resetControlPoints(previous); - - if ((origin instanceof CurveTo || origin instanceof SmoothCurveTo) - && (result instanceof CurveTo || result instanceof SmoothCurveTo)) { - if (result instanceof CurveTo) { - result.values[0] = controlPoints[0].x; - result.values[1] = controlPoints[0].y; - result.values[2] = controlPoints[1].x; - result.values[3] = controlPoints[1].y; - } - if (result instanceof SmoothCurveTo) { - result.values[0] = controlPoints[1].x; - result.values[1] = controlPoints[1].y; - } - } - - if ((origin instanceof QuadraticBezierCurveTo || origin instanceof SmoothQuadraticBezierCurveTo) - && (result instanceof QuadraticBezierCurveTo)) { - result.values[0] = controlPoints[0].x; - result.values[1] = controlPoints[0].y; - } - - if (newType !== absoluteType) { - result.setRelative(true); - } - return result; - } - - public refreshAbsolutePoints(origin: Point, previous: SvgItem | null) { - this.previousPoint = previous ? previous.targetLocation() : new Point(0, 0); - this.absolutePoints = []; - let current = previous ? previous.targetLocation() : new Point(0, 0); - if (!this.relative) { - current = new Point(0, 0); - } - for (let i = 0 ; i < this.values.length - 1 ; i += 2) { - this.absolutePoints.push( - new SvgPoint(current.x + this.values[i], current.y + this.values[i + 1]) - ); - } - } - - public setRelative(newRelative: boolean) { - if (this.relative !== newRelative) { - this.relative = false; - if (newRelative) { - this.translate(-this.previousPoint.x, -this.previousPoint.y); - this.relative = true; - } else { - this.translate(this.previousPoint.x, this.previousPoint.y); - } - } - } - - public refreshAbsoluteControlPoints(origin: Point, previous: SvgItem | null) { - this.absoluteControlPoints = []; - } - - public resetControlPoints(previousTarget: SvgItem) { - } - - public translate(x: number, y: number, force = false) { - if (!this.relative || force) { - this.values.forEach( (val, idx) => { - this.values[idx] = val + (idx % 2 === 0 ? x : y); - }); - } - } - - public scale(kx: number, ky: number) { - this.values.forEach( (val, idx) => { - this.values[idx] = val * (idx % 2 === 0 ? kx : ky); - }); - } - - public targetLocation(): SvgPoint { - const l = this.absolutePoints.length; - return this.absolutePoints[l - 1]; - } - - public setTargetLocation(pts: Point) { - const loc = this.targetLocation(); - const dx = pts.x - loc.x; - const dy = pts.y - loc.y; - const l = this.values.length; - this.values[l - 2] += dx; - this.values[l - 1] += dy; - } - - public setControlLocation(idx: number, pts: Point) { - const loc = this.absolutePoints[idx]; - const dx = pts.x - loc.x; - const dy = pts.y - loc.y; - this.values[2 * idx] += dx; - this.values[2 * idx + 1] += dy; - } - - public controlLocations(): SvgControlPoint[] { - return this.absoluteControlPoints; - } - - public getType(): string { - let typeKey = (this.constructor as any).key as string; - if (this.relative) { - typeKey = typeKey.toLowerCase(); - } - return typeKey; - } - - public asStandaloneString(): string { - return ['M', - this.previousPoint.x, - this.previousPoint.y, - this.getType(), - ...this.values - ].join(' '); - } - - public asString(decimals: number = 4, minify: boolean = false, trailingItems: SvgItem[] = []): string { - const strValues = [this.values, ...trailingItems.map(it => it.values)] - .reduce((acc, val) => acc.concat(val), []) - .map(it => formatNumber(it, decimals, minify)); - return [this.getType(), ...strValues].join(' '); + return result; + } + + public static MakeFrom(origin: SvgItem, previous: SvgItem, newType: string) { + const target = origin.targetLocation(); + const x = target.x.toString(); + const y = target.y.toString(); + let values: string[] = []; + const absoluteType = newType.toUpperCase(); + switch (absoluteType) { + case MoveTo.key: + values = [MoveTo.key, x, y]; + break; + case LineTo.key: + values = [LineTo.key, x, y]; + break; + case HorizontalLineTo.key: + values = [HorizontalLineTo.key, x]; + break; + case VerticalLineTo.key: + values = [VerticalLineTo.key, y]; + break; + case ClosePath.key: + values = [ClosePath.key]; + break; + case CurveTo.key: + values = [CurveTo.key, '0', '0', '0', '0', x, y]; + break; + case SmoothCurveTo.key: + values = [SmoothCurveTo.key, '0', '0', x, y]; + break; + case QuadraticBezierCurveTo.key: + values = [QuadraticBezierCurveTo.key, '0', '0', x, y]; + break; + case SmoothQuadraticBezierCurveTo.key: + values = [SmoothQuadraticBezierCurveTo.key, x, y]; + break; + case EllipticalArcTo.key: + values = [EllipticalArcTo.key, '1', '1', '0', '0', '0', x, y]; + break; + } + const result = SvgItem.Make(values); + + const controlPoints = origin.absoluteControlPoints; + + result.previousPoint = previous.targetLocation(); + result.absolutePoints = [target]; + result.resetControlPoints(previous); + + if ( + (origin instanceof CurveTo || origin instanceof SmoothCurveTo) && + (result instanceof CurveTo || result instanceof SmoothCurveTo) + ) { + if (result instanceof CurveTo) { + result.values[0] = controlPoints[0].x; + result.values[1] = controlPoints[0].y; + result.values[2] = controlPoints[1].x; + result.values[3] = controlPoints[1].y; + } + if (result instanceof SmoothCurveTo) { + result.values[0] = controlPoints[1].x; + result.values[1] = controlPoints[1].y; + } + } + + if ( + (origin instanceof QuadraticBezierCurveTo || + origin instanceof SmoothQuadraticBezierCurveTo) && + result instanceof QuadraticBezierCurveTo + ) { + result.values[0] = controlPoints[0].x; + result.values[1] = controlPoints[0].y; + } + + if (newType !== absoluteType) { + result.setRelative(true); } + return result; + } + + public refreshAbsolutePoints(origin: Point, previous: SvgItem | null) { + this.previousPoint = previous ? previous.targetLocation() : new Point(0, 0); + this.absolutePoints = []; + let current = previous ? previous.targetLocation() : new Point(0, 0); + if (!this.relative) { + current = new Point(0, 0); + } + for (let i = 0; i < this.values.length - 1; i += 2) { + this.absolutePoints.push( + new SvgPoint(current.x + this.values[i], current.y + this.values[i + 1]) + ); + } + } + + public setRelative(newRelative: boolean) { + if (this.relative !== newRelative) { + this.relative = false; + if (newRelative) { + this.translate(-this.previousPoint.x, -this.previousPoint.y); + this.relative = true; + } else { + this.translate(this.previousPoint.x, this.previousPoint.y); + } + } + } + + public refreshAbsoluteControlPoints(origin: Point, previous: SvgItem | null) { + this.absoluteControlPoints = []; + } + + public resetControlPoints(previousTarget: SvgItem) {} + + public translate(x: number, y: number, force = false) { + if (!this.relative || force) { + this.values.forEach((val, idx) => { + this.values[idx] = val + (idx % 2 === 0 ? x : y); + }); + } + } + + public scale(kx: number, ky: number) { + this.values.forEach((val, idx) => { + this.values[idx] = val * (idx % 2 === 0 ? kx : ky); + }); + } + + public targetLocation(): SvgPoint { + const l = this.absolutePoints.length; + return this.absolutePoints[l - 1]; + } + + public setTargetLocation(pts: Point) { + const loc = this.targetLocation(); + const dx = pts.x - loc.x; + const dy = pts.y - loc.y; + const l = this.values.length; + this.values[l - 2] += dx; + this.values[l - 1] += dy; + } + + public setControlLocation(idx: number, pts: Point) { + const loc = this.absolutePoints[idx]; + const dx = pts.x - loc.x; + const dy = pts.y - loc.y; + this.values[2 * idx] += dx; + this.values[2 * idx + 1] += dy; + } + + public controlLocations(): SvgControlPoint[] { + return this.absoluteControlPoints; + } + + public getType(): string { + let typeKey = (this.constructor as any).key as string; + if (this.relative) { + typeKey = typeKey.toLowerCase(); + } + return typeKey; + } + + public asStandaloneString(): string { + return [ + 'M', + this.previousPoint.x, + this.previousPoint.y, + this.getType(), + ...this.values, + ].join(' '); + } + + public asString( + decimals: number = 4, + minify: boolean = false, + trailingItems: SvgItem[] = [] + ): string { + const strValues = [this.values, ...trailingItems.map((it) => it.values)] + .reduce((acc, val) => acc.concat(val), []) + .map((it) => formatNumber(it, decimals, minify)); + return [this.getType(), ...strValues].join(' '); + } } class DummySvgItem extends SvgItem { - constructor() { - super([], false); - } + constructor() { + super([], false); + } } class MoveTo extends SvgItem { - static readonly key = 'M'; + static readonly key = 'M'; } class LineTo extends SvgItem { - static readonly key = 'L'; + static readonly key = 'L'; } class CurveTo extends SvgItem { - static readonly key = 'C'; - public refreshAbsoluteControlPoints(origin: Point, previousTarget: SvgItem | null) { - if(!previousTarget) { - throw 'Invalid path'; - } - this.absoluteControlPoints = [ - new SvgControlPoint(this.absolutePoints[0], [previousTarget.targetLocation()]), - new SvgControlPoint(this.absolutePoints[1], [this.targetLocation()]) - ]; - } - public resetControlPoints(previousTarget: SvgItem) { - const a = previousTarget.targetLocation(); - const b = this.targetLocation(); - const d = this.relative ? a : new Point(0, 0); - this.values[0] = 2 * a.x / 3 + b.x / 3 - d.x; - this.values[1] = 2 * a.y / 3 + b.y / 3 - d.y; - this.values[2] = a.x / 3 + 2 * b.x / 3 - d.x; - this.values[3] = a.y / 3 + 2 * b.y / 3 - d.y; - } + static readonly key = 'C'; + public refreshAbsoluteControlPoints( + origin: Point, + previousTarget: SvgItem | null + ) { + if (!previousTarget) { + throw 'Invalid path'; + } + this.absoluteControlPoints = [ + new SvgControlPoint(this.absolutePoints[0], [ + previousTarget.targetLocation(), + ]), + new SvgControlPoint(this.absolutePoints[1], [this.targetLocation()]), + ]; + } + public resetControlPoints(previousTarget: SvgItem) { + const a = previousTarget.targetLocation(); + const b = this.targetLocation(); + const d = this.relative ? a : new Point(0, 0); + this.values[0] = (2 * a.x) / 3 + b.x / 3 - d.x; + this.values[1] = (2 * a.y) / 3 + b.y / 3 - d.y; + this.values[2] = a.x / 3 + (2 * b.x) / 3 - d.x; + this.values[3] = a.y / 3 + (2 * b.y) / 3 - d.y; + } } class SmoothCurveTo extends SvgItem { - static readonly key = 'S'; - public refreshAbsoluteControlPoints(origin: Point, previousTarget: SvgItem | null) { - this.absoluteControlPoints = []; - if ((previousTarget instanceof CurveTo || previousTarget instanceof SmoothCurveTo)) { - const prevLoc = previousTarget.targetLocation(); - const prevControl = previousTarget.absoluteControlPoints[1]; - const pts = new Point(2 * prevLoc.x - prevControl.x, 2 * prevLoc.y - prevControl.y); - this.absoluteControlPoints.push( - new SvgControlPoint(pts, [prevLoc], false) - ); - } else { - const current = previousTarget ? previousTarget.targetLocation() : new Point(0, 0); - const pts = new Point(current.x, current.y); - this.absoluteControlPoints.push( - new SvgControlPoint(pts, [], false) - ); - } - this.absoluteControlPoints.push( - new SvgControlPoint(this.absolutePoints[0], [this.targetLocation()]), - ); - } - public asStandaloneString(): string { - return [ - 'M', - this.previousPoint.x, - this.previousPoint.y, - 'C', - this.absoluteControlPoints[0].x, - this.absoluteControlPoints[0].y, - this.absoluteControlPoints[1].x, - this.absoluteControlPoints[1].y, - this.absolutePoints[1].x, - this.absolutePoints[1].y - ].join(' '); - } - public resetControlPoints(previousTarget: SvgItem) { - const a = previousTarget.targetLocation(); - const b = this.targetLocation(); - const d = this.relative ? a : new Point(0, 0); - this.values[0] = a.x / 3 + 2 * b.x / 3 - d.x; - this.values[1] = a.y / 3 + 2 * b.y / 3 - d.y; - } - public setControlLocation(idx: number, pts: Point) { - const loc = this.absoluteControlPoints[1]; - const dx = pts.x - loc.x; - const dy = pts.y - loc.y; - this.values[0] += dx; - this.values[1] += dy; - } + static readonly key = 'S'; + public refreshAbsoluteControlPoints( + origin: Point, + previousTarget: SvgItem | null + ) { + this.absoluteControlPoints = []; + if ( + previousTarget instanceof CurveTo || + previousTarget instanceof SmoothCurveTo + ) { + const prevLoc = previousTarget.targetLocation(); + const prevControl = previousTarget.absoluteControlPoints[1]; + const pts = new Point( + 2 * prevLoc.x - prevControl.x, + 2 * prevLoc.y - prevControl.y + ); + this.absoluteControlPoints.push( + new SvgControlPoint(pts, [prevLoc], false) + ); + } else { + const current = previousTarget + ? previousTarget.targetLocation() + : new Point(0, 0); + const pts = new Point(current.x, current.y); + this.absoluteControlPoints.push(new SvgControlPoint(pts, [], false)); + } + this.absoluteControlPoints.push( + new SvgControlPoint(this.absolutePoints[0], [this.targetLocation()]) + ); + } + public asStandaloneString(): string { + return [ + 'M', + this.previousPoint.x, + this.previousPoint.y, + 'C', + this.absoluteControlPoints[0].x, + this.absoluteControlPoints[0].y, + this.absoluteControlPoints[1].x, + this.absoluteControlPoints[1].y, + this.absolutePoints[1].x, + this.absolutePoints[1].y, + ].join(' '); + } + public resetControlPoints(previousTarget: SvgItem) { + const a = previousTarget.targetLocation(); + const b = this.targetLocation(); + const d = this.relative ? a : new Point(0, 0); + this.values[0] = a.x / 3 + (2 * b.x) / 3 - d.x; + this.values[1] = a.y / 3 + (2 * b.y) / 3 - d.y; + } + public setControlLocation(idx: number, pts: Point) { + const loc = this.absoluteControlPoints[1]; + const dx = pts.x - loc.x; + const dy = pts.y - loc.y; + this.values[0] += dx; + this.values[1] += dy; + } } class QuadraticBezierCurveTo extends SvgItem { - static readonly key = 'Q'; - public refreshAbsoluteControlPoints(origin: Point, previousTarget: SvgItem | null) { - if(!previousTarget) { - throw 'Invalid path'; - } - this.absoluteControlPoints = [ - new SvgControlPoint(this.absolutePoints[0], [previousTarget.targetLocation(), this.targetLocation()]) - ]; - } - public resetControlPoints(previousTarget: SvgItem) { - const a = previousTarget.targetLocation(); - const b = this.targetLocation(); - const d = this.relative ? a : new Point(0, 0); - this.values[0] = a.x / 2 + b.x / 2 - d.x; - this.values[1] = a.y / 2 + b.y / 2 - d.y; - } + static readonly key = 'Q'; + public refreshAbsoluteControlPoints( + origin: Point, + previousTarget: SvgItem | null + ) { + if (!previousTarget) { + throw 'Invalid path'; + } + this.absoluteControlPoints = [ + new SvgControlPoint(this.absolutePoints[0], [ + previousTarget.targetLocation(), + this.targetLocation(), + ]), + ]; + } + public resetControlPoints(previousTarget: SvgItem) { + const a = previousTarget.targetLocation(); + const b = this.targetLocation(); + const d = this.relative ? a : new Point(0, 0); + this.values[0] = a.x / 2 + b.x / 2 - d.x; + this.values[1] = a.y / 2 + b.y / 2 - d.y; + } } class SmoothQuadraticBezierCurveTo extends SvgItem { - static readonly key = 'T'; - public refreshAbsoluteControlPoints(origin: Point, previousTarget: SvgItem | null) { - if (!(previousTarget instanceof QuadraticBezierCurveTo || previousTarget instanceof SmoothQuadraticBezierCurveTo)) { - const previous = previousTarget ? previousTarget.targetLocation() : new Point(0, 0); - const pts = new Point(previous.x, previous.y); - this.absoluteControlPoints = [ - new SvgControlPoint(pts, [], false) - ]; - } else { - const prevLoc = previousTarget.targetLocation(); - const prevControl = previousTarget.absoluteControlPoints[0]; - const pts = new Point(2 * prevLoc.x - prevControl.x, 2 * prevLoc.y - prevControl.y); - this.absoluteControlPoints = [ - new SvgControlPoint(pts, [prevLoc, this.targetLocation()], false) - ]; - } - } - public asStandaloneString(): string { - return [ - 'M', - this.previousPoint.x, - this.previousPoint.y, - 'Q', - this.absoluteControlPoints[0].x, - this.absoluteControlPoints[0].y, - this.absolutePoints[0].x, - this.absolutePoints[0].y - ].join(' '); - } + static readonly key = 'T'; + public refreshAbsoluteControlPoints( + origin: Point, + previousTarget: SvgItem | null + ) { + if ( + !( + previousTarget instanceof QuadraticBezierCurveTo || + previousTarget instanceof SmoothQuadraticBezierCurveTo + ) + ) { + const previous = previousTarget + ? previousTarget.targetLocation() + : new Point(0, 0); + const pts = new Point(previous.x, previous.y); + this.absoluteControlPoints = [new SvgControlPoint(pts, [], false)]; + } else { + const prevLoc = previousTarget.targetLocation(); + const prevControl = previousTarget.absoluteControlPoints[0]; + const pts = new Point( + 2 * prevLoc.x - prevControl.x, + 2 * prevLoc.y - prevControl.y + ); + this.absoluteControlPoints = [ + new SvgControlPoint(pts, [prevLoc, this.targetLocation()], false), + ]; + } + } + public asStandaloneString(): string { + return [ + 'M', + this.previousPoint.x, + this.previousPoint.y, + 'Q', + this.absoluteControlPoints[0].x, + this.absoluteControlPoints[0].y, + this.absolutePoints[0].x, + this.absolutePoints[0].y, + ].join(' '); + } } class ClosePath extends SvgItem { - static readonly key = 'Z'; - public refreshAbsolutePoints(origin: Point, previous: SvgItem | null) { - this.previousPoint = previous ? previous.targetLocation() : new Point(0, 0); - this.absolutePoints = [new SvgPoint(origin.x, origin.y, false)]; - } - + static readonly key = 'Z'; + public refreshAbsolutePoints(origin: Point, previous: SvgItem | null) { + this.previousPoint = previous ? previous.targetLocation() : new Point(0, 0); + this.absolutePoints = [new SvgPoint(origin.x, origin.y, false)]; + } } class HorizontalLineTo extends SvgItem { - static readonly key = 'H'; - public refreshAbsolutePoints(origin: Point, previous: SvgItem | null) { - this.previousPoint = previous ? previous.targetLocation() : new Point(0, 0); - if (this.relative) { - this.absolutePoints = [new SvgPoint(this.values[0] + this.previousPoint.x, this.previousPoint.y)]; - } else { - this.absolutePoints = [new SvgPoint(this.values[0], this.previousPoint.y)]; - } - } - public setTargetLocation(pts: Point) { - const loc = this.targetLocation(); - const dx = pts.x - loc.x; - this.values[0] += dx; - } + static readonly key = 'H'; + public refreshAbsolutePoints(origin: Point, previous: SvgItem | null) { + this.previousPoint = previous ? previous.targetLocation() : new Point(0, 0); + if (this.relative) { + this.absolutePoints = [ + new SvgPoint( + this.values[0] + this.previousPoint.x, + this.previousPoint.y + ), + ]; + } else { + this.absolutePoints = [ + new SvgPoint(this.values[0], this.previousPoint.y), + ]; + } + } + public setTargetLocation(pts: Point) { + const loc = this.targetLocation(); + const dx = pts.x - loc.x; + this.values[0] += dx; + } } class VerticalLineTo extends SvgItem { - static readonly key = 'V'; - public translate(x: number, y: number, force = false) { - if (!this.relative) { - this.values[0] += y; - } - } - public scale(kx: number, ky: number) { - this.values[0] *= ky; - } - public refreshAbsolutePoints(origin: Point, previous: SvgItem | null) { - this.previousPoint = previous ? previous.targetLocation() : new Point(0, 0); - if (this.relative) { - this.absolutePoints = [new SvgPoint(this.previousPoint.x, this.values[0] + this.previousPoint.y)]; - } else { - this.absolutePoints = [new SvgPoint(this.previousPoint.x, this.values[0])]; - } - } - public setTargetLocation(pts: Point) { - const loc = this.targetLocation(); - const dy = pts.y - loc.y; - this.values[0] += dy; - } + static readonly key = 'V'; + public translate(x: number, y: number, force = false) { + if (!this.relative) { + this.values[0] += y; + } + } + public scale(kx: number, ky: number) { + this.values[0] *= ky; + } + public refreshAbsolutePoints(origin: Point, previous: SvgItem | null) { + this.previousPoint = previous ? previous.targetLocation() : new Point(0, 0); + if (this.relative) { + this.absolutePoints = [ + new SvgPoint( + this.previousPoint.x, + this.values[0] + this.previousPoint.y + ), + ]; + } else { + this.absolutePoints = [ + new SvgPoint(this.previousPoint.x, this.values[0]), + ]; + } + } + public setTargetLocation(pts: Point) { + const loc = this.targetLocation(); + const dy = pts.y - loc.y; + this.values[0] += dy; + } } class EllipticalArcTo extends SvgItem { - static readonly key = 'A'; - public translate(x: number, y: number, force = false) { - if (!this.relative) { - this.values[5] += x; - this.values[6] += y; - } - } - public scale(kx: number, ky: number) { - const a = this.values[0]; - const b = this.values[1]; - const angle = Math.PI * this.values[2] / 180.; - const cos = Math.cos(angle); - const sin = Math.sin(angle); - const A = b * b * ky * ky * cos * cos + a * a * ky * ky * sin * sin; - const B = 2 * kx * ky * cos * sin * (b * b - a * a ); - const C = a * a * kx * kx * cos * cos + b * b * kx * kx * sin * sin; - const F = -(a * a * b * b * kx * kx * ky * ky); - const det = B * B - 4 * A * C; - const val1 = Math.sqrt((A - C) * (A - C) + B * B); - - // New rotation: - this.values[2] = B !== 0 ? Math.atan((C - A - val1) / B) * 180 / Math.PI : (A < C ? 0 : 90); - - // New radius-x, radius-y - this.values[0] = -Math.sqrt(2 * det * F * ((A + C) + val1)) / det; - this.values[1] = -Math.sqrt(2 * det * F * ((A + C) - val1)) / det; - - // New target - this.values[5] *= kx; - this.values[6] *= ky; - - // New sweep flag - this.values[4] = kx * ky >= 0 ? this.values[4] : 1 - this.values[4]; - } - public refreshAbsolutePoints(origin: Point, previous: SvgItem | null) { - this.previousPoint = previous ? previous.targetLocation() : new Point(0, 0); - if (this.relative) { - this.absolutePoints = [new SvgPoint(this.values[5] + this.previousPoint.x, this.values[6] + this.previousPoint.y)]; - } else { - this.absolutePoints = [new SvgPoint(this.values[5], this.values[6])]; - } - } - - public asString(decimals: number = 4, minify: boolean = false, trailingItems: SvgItem[] = []): string { - if (!minify) { - return super.asString(decimals, minify, trailingItems); - } else { - const strValues = [this.values, ...trailingItems.map(it => it.values)] - .map(it => it.map(it2 => formatNumber(it2, decimals, minify))) - .map(v => `${v[0]} ${v[1]} ${v[2]} ${v[3]}${v[4]}${v[5]} ${v[6]}`); - return [this.getType(), ...strValues].join(' '); - } - } + static readonly key = 'A'; + public translate(x: number, y: number, force = false) { + if (!this.relative) { + this.values[5] += x; + this.values[6] += y; + } + } + public scale(kx: number, ky: number) { + const a = this.values[0]; + const b = this.values[1]; + const angle = (Math.PI * this.values[2]) / 180; + const cos = Math.cos(angle); + const sin = Math.sin(angle); + const A = b * b * ky * ky * cos * cos + a * a * ky * ky * sin * sin; + const B = 2 * kx * ky * cos * sin * (b * b - a * a); + const C = a * a * kx * kx * cos * cos + b * b * kx * kx * sin * sin; + const F = -(a * a * b * b * kx * kx * ky * ky); + const det = B * B - 4 * A * C; + const val1 = Math.sqrt((A - C) * (A - C) + B * B); + + // New rotation: + this.values[2] = + B !== 0 + ? (Math.atan((C - A - val1) / B) * 180) / Math.PI + : A < C + ? 0 + : 90; + + // New radius-x, radius-y + this.values[0] = -Math.sqrt(2 * det * F * (A + C + val1)) / det; + this.values[1] = -Math.sqrt(2 * det * F * (A + C - val1)) / det; + + // New target + this.values[5] *= kx; + this.values[6] *= ky; + + // New sweep flag + this.values[4] = kx * ky >= 0 ? this.values[4] : 1 - this.values[4]; + } + public refreshAbsolutePoints(origin: Point, previous: SvgItem | null) { + this.previousPoint = previous ? previous.targetLocation() : new Point(0, 0); + if (this.relative) { + this.absolutePoints = [ + new SvgPoint( + this.values[5] + this.previousPoint.x, + this.values[6] + this.previousPoint.y + ), + ]; + } else { + this.absolutePoints = [new SvgPoint(this.values[5], this.values[6])]; + } + } + + public asString( + decimals: number = 4, + minify: boolean = false, + trailingItems: SvgItem[] = [] + ): string { + if (!minify) { + return super.asString(decimals, minify, trailingItems); + } else { + const strValues = [this.values, ...trailingItems.map((it) => it.values)] + .map((it) => it.map((it2) => formatNumber(it2, decimals, minify))) + .map((v) => `${v[0]} ${v[1]} ${v[2]} ${v[3]}${v[4]}${v[5]} ${v[6]}`); + return [this.getType(), ...strValues].join(' '); + } + } } - export class Svg { - path: SvgItem[]; - - constructor(path: string) { - const rawPath = SvgParser.parse(path); - this.path = rawPath.map( it => SvgItem.Make(it) ); - this.refreshAbsolutePositions(); - } - - translate(dx: number, dy: number): Svg { - this.path.forEach( (it, idx) => { - it.translate(dx, dy, idx === 0); - }); - this.refreshAbsolutePositions(); - return this; - } - - scale(kx: number, ky: number): Svg { - this.path.forEach( (it) => { - it.scale(kx, ky); - }); - this.refreshAbsolutePositions(); - return this; - } - - setRelative(newRelative: boolean) { - this.path.forEach( (it) => { - it.setRelative(newRelative); - }); - this.refreshAbsolutePositions(); - return this; - } - - delete(item: SvgItem) { - const idx = this.path.indexOf(item); - if (idx !== -1) { - this.path.splice(idx, 1); - this.refreshAbsolutePositions(); - } - return this; - } - - insert(item: SvgItem, after?: SvgItem) { - const idx = after ? this.path.indexOf(after) : -1; - if (idx !== -1) { - this.path.splice(idx + 1, 0, item); - } else { - this.path.push(item); - } - this.refreshAbsolutePositions(); - } - - changeType(item: SvgItem, newType: string): SvgItem | null { - const idx = this.path.indexOf(item); - if (idx > 0) { - const previous = this.path[idx - 1]; - this.path[idx] = SvgItem.MakeFrom(item, previous, newType); - this.refreshAbsolutePositions(); - return this.path[idx]; - } - return null; - } - - asString(decimals: number = 4, minify: boolean = false): string { - return this.path - .reduce((acc: {type?: string, item: SvgItem, trailing: SvgItem[]}[], it: SvgItem) => { - // Group together the items that can be merged (M 0 0 L 1 1 => M 0 0 1 1) - const type = it.getType(); - if (minify && acc.length > 0) { - const last = acc[acc.length - 1]; - if (last.type === type) { - last.trailing.push(it); - return acc; - } - } - acc.push({ - type: type === 'm' ? 'l' : (type === 'M' ? 'L' : type), - item: it, - trailing: [] - }); - return acc; - }, []) - .map(it => { - const str = it.item.asString(decimals, minify, it.trailing); - if (minify) { - return str - .replace(/^([a-z]) /i, '$1') - .replace(/ -/g, '-') - .replace(/(\.[0-9]+) (?=\.)/g, '$1'); - } else { - return str; + path: SvgItem[]; + + constructor(path: string) { + const rawPath = SvgParser.parse(path); + this.path = rawPath.map((it) => SvgItem.Make(it)); + this.refreshAbsolutePositions(); + } + + translate(dx: number, dy: number): Svg { + this.path.forEach((it, idx) => { + it.translate(dx, dy, idx === 0); + }); + this.refreshAbsolutePositions(); + return this; + } + + scale(kx: number, ky: number): Svg { + this.path.forEach((it) => { + it.scale(kx, ky); + }); + this.refreshAbsolutePositions(); + return this; + } + + setRelative(newRelative: boolean) { + this.path.forEach((it) => { + it.setRelative(newRelative); + }); + this.refreshAbsolutePositions(); + return this; + } + + delete(item: SvgItem) { + const idx = this.path.indexOf(item); + if (idx !== -1) { + this.path.splice(idx, 1); + this.refreshAbsolutePositions(); + } + return this; + } + + insert(item: SvgItem, after?: SvgItem) { + const idx = after ? this.path.indexOf(after) : -1; + if (idx !== -1) { + this.path.splice(idx + 1, 0, item); + } else { + this.path.push(item); + } + this.refreshAbsolutePositions(); + } + + changeType(item: SvgItem, newType: string): SvgItem | null { + const idx = this.path.indexOf(item); + if (idx > 0) { + const previous = this.path[idx - 1]; + this.path[idx] = SvgItem.MakeFrom(item, previous, newType); + this.refreshAbsolutePositions(); + return this.path[idx]; + } + return null; + } + + asString(decimals: number = 4, minify: boolean = false): string { + return this.path + .reduce( + ( + acc: {type?: string; item: SvgItem; trailing: SvgItem[]}[], + it: SvgItem + ) => { + // Group together the items that can be merged (M 0 0 L 1 1 => M 0 0 1 1) + const type = it.getType(); + if (minify && acc.length > 0) { + const last = acc[acc.length - 1]; + if (last.type === type) { + last.trailing.push(it); + return acc; } - }).join(minify ? '' : ' '); - } - - targetLocations(): SvgPoint[] { - return this.path.map((it) => it.targetLocation() ); - } - - controlLocations(): SvgControlPoint[] { - let result: SvgControlPoint[] = []; - for (let i = 1 ; i < this.path.length ; ++i) { - const controls = this.path[i].controlLocations(); - controls.forEach((it, idx) => { - it.subIndex = idx; - }); - result = [...result, ...controls]; - } - return result; - } - - - setLocation(ptReference: SvgPoint, to: Point) { - if (ptReference instanceof SvgControlPoint) { - ptReference.itemReference.setControlLocation(ptReference.subIndex, to); + } + acc.push({ + type: type === 'm' ? 'l' : type === 'M' ? 'L' : type, + item: it, + trailing: [], + }); + return acc; + }, + [] + ) + .map((it) => { + const str = it.item.asString(decimals, minify, it.trailing); + if (minify) { + return str + .replace(/^([a-z]) /i, '$1') + .replace(/ -/g, '-') + .replace(/(\.[0-9]+) (?=\.)/g, '$1'); } else { - ptReference.itemReference.setTargetLocation(to); + return str; } - this.refreshAbsolutePositions(); + }) + .join(minify ? '' : ' '); + } + + targetLocations(): SvgPoint[] { + return this.path.map((it) => it.targetLocation()); + } + + controlLocations(): SvgControlPoint[] { + let result: SvgControlPoint[] = []; + for (let i = 1; i < this.path.length; ++i) { + const controls = this.path[i].controlLocations(); + controls.forEach((it, idx) => { + it.subIndex = idx; + }); + result = [...result, ...controls]; } + return result; + } + setLocation(ptReference: SvgPoint, to: Point) { + if (ptReference instanceof SvgControlPoint) { + ptReference.itemReference.setControlLocation(ptReference.subIndex, to); + } else { + ptReference.itemReference.setTargetLocation(to); + } + this.refreshAbsolutePositions(); + } - refreshAbsolutePositions() { - let previous: SvgItem | null = null; - let origin = new Point(0, 0); - for (const item of this.path) { - item.refreshAbsolutePoints(origin, previous); - item.refreshAbsoluteControlPoints(origin, previous); + refreshAbsolutePositions() { + let previous: SvgItem | null = null; + let origin = new Point(0, 0); + for (const item of this.path) { + item.refreshAbsolutePoints(origin, previous); + item.refreshAbsoluteControlPoints(origin, previous); - item.absolutePoints.forEach(it => it.itemReference = item ); - item.absoluteControlPoints.forEach(it => it.itemReference = item); + item.absolutePoints.forEach((it) => (it.itemReference = item)); + item.absoluteControlPoints.forEach((it) => (it.itemReference = item)); - if (item instanceof MoveTo || item instanceof ClosePath) { - origin = item.targetLocation(); - } - previous = item; - } + if (item instanceof MoveTo || item instanceof ClosePath) { + origin = item.targetLocation(); + } + previous = item; } + } } diff --git a/src/app/upload-image/upload-image-dialog.component.html b/src/app/upload-image/upload-image-dialog.component.html index b05cf68a..b1b1a9e8 100644 --- a/src/app/upload-image/upload-image-dialog.component.html +++ b/src/app/upload-image/upload-image-dialog.component.html @@ -1,34 +1,59 @@

Import image

-
- + - {{name}} + {{ name }}
x - + y - + width - + height - +
- Preserve Aspect Ratio + + Preserve Aspect Ratio +
- +
- -
\ No newline at end of file + + diff --git a/src/app/upload-image/upload-image-dialog.component.scss b/src/app/upload-image/upload-image-dialog.component.scss index 689c5c30..ae1eb778 100644 --- a/src/app/upload-image/upload-image-dialog.component.scss +++ b/src/app/upload-image/upload-image-dialog.component.scss @@ -1,25 +1,25 @@ .upload-image-dialog-content { - display: flex; - justify-content: space-between; - flex-wrap: wrap; - padding-bottom:8px; + display: flex; + justify-content: space-between; + flex-wrap: wrap; + padding-bottom: 8px; - .upload-button-row { - width: 100%; - padding-bottom:16px; - display: flex; - align-items: center; - .preview { - max-height: 36px; - max-width: 60px; - margin-left:16px; - } - .filename { - margin-left:16px; - font-style: italic; - } + .upload-button-row { + width: 100%; + padding-bottom: 16px; + display: flex; + align-items: center; + .preview { + max-height: 36px; + max-width: 60px; + margin-left: 16px; } - mat-form-field { - width: calc(50% - 4px); + .filename { + margin-left: 16px; + font-style: italic; } -} \ No newline at end of file + } + mat-form-field { + width: calc(50% - 4px); + } +} diff --git a/src/app/upload-image/upload-image.component.html b/src/app/upload-image/upload-image.component.html index 17e14893..6ec09285 100644 --- a/src/app/upload-image/upload-image.component.html +++ b/src/app/upload-image/upload-image.component.html @@ -1,8 +1,9 @@ - \ No newline at end of file + add_photo_alternate + diff --git a/src/app/upload-image/upload-image.component.spec.ts b/src/app/upload-image/upload-image.component.spec.ts index a182d6b4..85e5566d 100644 --- a/src/app/upload-image/upload-image.component.spec.ts +++ b/src/app/upload-image/upload-image.component.spec.ts @@ -1,8 +1,8 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatIconModule } from '@angular/material/icon'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatIconModule} from '@angular/material/icon'; -import { UploadImageComponent } from './upload-image.component'; +import {UploadImageComponent} from './upload-image.component'; describe('UploadImageComponent', () => { let component: UploadImageComponent; @@ -10,10 +10,9 @@ describe('UploadImageComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ MatDialogModule, MatIconModule ], - declarations: [ UploadImageComponent ] - }) - .compileComponents(); + imports: [MatDialogModule, MatIconModule], + declarations: [UploadImageComponent], + }).compileComponents(); }); beforeEach(() => { diff --git a/src/app/upload-image/upload-image.component.ts b/src/app/upload-image/upload-image.component.ts index 918d678e..9fb53b6a 100644 --- a/src/app/upload-image/upload-image.component.ts +++ b/src/app/upload-image/upload-image.component.ts @@ -1,12 +1,12 @@ -import { Component, Output, EventEmitter } from '@angular/core'; -import { MatDialog, MatDialogRef} from '@angular/material/dialog'; -import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; -import { Image } from '../image'; +import {Component, Output, EventEmitter} from '@angular/core'; +import {MatDialog, MatDialogRef} from '@angular/material/dialog'; +import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; +import {Image} from '../image'; @Component({ selector: 'app-upload-image-dialog', templateUrl: 'upload-image-dialog.component.html', - styleUrls: ['./upload-image-dialog.component.scss'] + styleUrls: ['./upload-image-dialog.component.scss'], }) export class UploadImageDialogComponent { data: string | null = null; @@ -21,15 +21,15 @@ export class UploadImageDialogComponent { constructor( public dialogRef: MatDialogRef, public domSanitizer: DomSanitizer - ) { - } + ) {} private importFile(file: File) { if (window.FileReader !== undefined) { const reader = new FileReader(); reader.onload = (e: any) => { this.data = e.target.result; - if(this.data) { - this.displayableData = this.domSanitizer.bypassSecurityTrustResourceUrl(this.data); + if (this.data) { + this.displayableData = + this.domSanitizer.bypassSecurityTrustResourceUrl(this.data); } }; this.name = file.name; @@ -49,12 +49,12 @@ export class UploadImageDialogComponent { x2: parseFloat(this.x) + parseFloat(this.width), y2: parseFloat(this.y) + parseFloat(this.height), preserveAspectRatio: this.preserveAspectRatio, - opacity:1.0 + opacity: 1.0, }); } onFileSelected(uploadInput: HTMLInputElement) { - if (typeof (FileReader) !== 'undefined') { - if(uploadInput.files) { + if (typeof FileReader !== 'undefined') { + if (uploadInput.files) { this.importFile(uploadInput.files[0]); } } else { @@ -62,7 +62,7 @@ export class UploadImageDialogComponent { } } onDrop(event: DragEvent) { - if(event.dataTransfer && event.dataTransfer.files) { + if (event.dataTransfer && event.dataTransfer.files) { const file = event.dataTransfer.files[0]; if (/^image\//.test(file.type)) { this.importFile(file); @@ -71,29 +71,27 @@ export class UploadImageDialogComponent { event.preventDefault(); } onDragOver(event: DragEvent) { - event.stopPropagation(); - event.preventDefault(); + event.stopPropagation(); + event.preventDefault(); } } @Component({ selector: 'app-upload-image', - templateUrl: './upload-image.component.html' + templateUrl: './upload-image.component.html', }) export class UploadImageComponent { @Output() addImage = new EventEmitter(); @Output() cancel = new EventEmitter(); - constructor( - public dialog: MatDialog, - ) {} + constructor(public dialog: MatDialog) {} openDialog(): void { const dialogRef = this.dialog.open(UploadImageDialogComponent, { width: '800px', - panelClass: 'dialog' + panelClass: 'dialog', }); - dialogRef.afterClosed().subscribe((result: Image) => { + dialogRef.afterClosed().subscribe((result: Image) => { if (result) { this.addImage.emit(result); } else { diff --git a/src/custom-theme.scss b/src/custom-theme.scss index 12914925..e137e953 100644 --- a/src/custom-theme.scss +++ b/src/custom-theme.scss @@ -19,10 +19,13 @@ $svg-path-editor-accent: mat.define-palette(mat.$blue-palette); $svg-path-editor-warn: mat.define-palette(mat.$red-palette); // Create the theme object (a Sass map containing all of the palettes). -$svg-path-editor-theme: mat.define-dark-theme($svg-path-editor-primary, $svg-path-editor-accent, $svg-path-editor-warn); +$svg-path-editor-theme: mat.define-dark-theme( + $svg-path-editor-primary, + $svg-path-editor-accent, + $svg-path-editor-warn +); // Include theme styles for core and each component used in your app. // Alternatively, you can import and @include the theme mixins for each component // that you are using. @include mat.all-component-themes($svg-path-editor-theme); - diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 3612073b..c9669790 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -1,3 +1,3 @@ export const environment = { - production: true + production: true, }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 30d7bccb..31cb7855 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -3,7 +3,7 @@ // The list of file replacements can be found in `angular.json`. export const environment = { - production: false + production: false, }; /* diff --git a/src/index.html b/src/index.html index 4df76e33..fd6c9bc1 100644 --- a/src/index.html +++ b/src/index.html @@ -1,29 +1,51 @@ - + - - - SvgPathEditor - - - - - - - - - - - - - - - - - - + + + SvgPathEditor + + + + + + + + + + + + + + + + + + diff --git a/src/main.ts b/src/main.ts index c7b673cf..c8a4ca73 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,13 @@ -import { enableProdMode } from '@angular/core'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import {enableProdMode} from '@angular/core'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; -import { AppModule } from './app/app.module'; -import { environment } from './environments/environment'; +import {AppModule} from './app/app.module'; +import {environment} from './environments/environment'; if (environment.production) { enableProdMode(); } -platformBrowserDynamic().bootstrapModule(AppModule) - .catch(err => console.error(err)); +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err) => console.error(err)); diff --git a/src/polyfills.ts b/src/polyfills.ts index 67581db7..7338712a 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -55,8 +55,7 @@ /*************************************************************************************************** * Zone JS is required by default for Angular itself. */ -import 'zone.js'; // Included with Angular CLI. - +import 'zone.js'; // Included with Angular CLI. /*************************************************************************************************** * APPLICATION IMPORTS diff --git a/src/styles.scss b/src/styles.scss index 5bc3f9ab..4b31b5bf 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,168 +1,167 @@ /* You can add global styles to this file, and also import other style files */ html { - font-family: Roboto, Arial, "Helvetica Neue", sans-serif; - scrollbar-color: #565656 #393939; - scrollbar-width: thin; - color:white; - background-color: #1e1e1e; -} -html, body { - margin:0; - padding:0; - height: 100%; - overflow: hidden; - /* Following lines could prevent elastic scrolling in mobile Safari, + font-family: Roboto, Arial, 'Helvetica Neue', sans-serif; + scrollbar-color: #565656 #393939; + scrollbar-width: thin; + color: white; + background-color: #1e1e1e; +} +html, +body { + margin: 0; + padding: 0; + height: 100%; + overflow: hidden; + /* Following lines could prevent elastic scrolling in mobile Safari, * but deteriorates performances a lot: * / width:100%; position: fixed; */ } - + ::-webkit-scrollbar { - width: 6px; + width: 6px; } ::-webkit-scrollbar-track { - background-color: #393939; + background-color: #393939; } ::-webkit-scrollbar-thumb { - background-color: #565656; - border-radius: 3px; - border: 0px solid #393939; + background-color: #565656; + border-radius: 3px; + border: 0px solid #393939; } ::-webkit-scrollbar-button { - width: 0; - height: 0; - display: none; + width: 0; + height: 0; + display: none; } ::-webkit-scrollbar-corner { - background-color:transparent; + background-color: transparent; } - - input.app-input, textarea.app-input { - color:white; - background-color: rgba(255,255,255,0.145); - border-style: solid; - border-width: 0 0 1px 0; - border-color: #c8c8c8; - border-radius: 2px 2px 0 0; - transition: border-width 100ms, padding-bottom 100ms; + color: white; + background-color: rgba(255, 255, 255, 0.145); + border-style: solid; + border-width: 0 0 1px 0; + border-color: #c8c8c8; + border-radius: 2px 2px 0 0; + transition: border-width 100ms, padding-bottom 100ms; } textarea.app-input, input.app-input { - margin: 0; - padding: 0 4px 1px 4px; + margin: 0; + padding: 0 4px 1px 4px; } input.app-input { - height: 20px; + height: 20px; } input.app-input:focus, textarea.app-input:focus, input.app-input:hover, textarea.app-input:hover { - border-width: 0 0 2px 0; - padding-bottom: 0; + border-width: 0 0 2px 0; + padding-bottom: 0; } input.app-input:hover, textarea.app-input:hover { - border-color: white; + border-color: white; } input.app-input:focus, textarea.app-input:focus { - outline: none; - border-color: #2196f3; + outline: none; + border-color: #2196f3; } button.mat-flat-button { - padding:0 8px !important; - height:32px; - line-height: 32px; - min-width: auto !important; + padding: 0 8px !important; + height: 32px; + line-height: 32px; + min-width: auto !important; } .row { - display: flex; - align-items: center; - padding-left:2px; - padding-right:2px; - >* { - margin-top: 1px; - margin-bottom: 1px; - } - >button.mat-flat-button { - margin:2px !important; - } - .small-checkbox label.mat-checkbox-layout { - display:flex !important; - } - >.mat-checkbox { - margin-left:2px; - margin-right:2px; - } + display: flex; + align-items: center; + padding-left: 2px; + padding-right: 2px; + > * { + margin-top: 1px; + margin-bottom: 1px; + } + > button.mat-flat-button { + margin: 2px !important; + } + .small-checkbox label.mat-checkbox-layout { + display: flex !important; + } + > .mat-checkbox { + margin-left: 2px; + margin-right: 2px; + } } .mat-checkbox-label { - font-size: 12px; + font-size: 12px; } button.mat-icon-button { - width:20px; - height:20px; - line-height: 20px !important; - font-size: 1px !important; - mat-icon { - width: 12px; - height: 12px; - line-height: 12px !important; - } + width: 20px; + height: 20px; + line-height: 20px !important; + font-size: 1px !important; + mat-icon { + width: 12px; + height: 12px; + line-height: 12px !important; + } } .input-block { - margin:2px; - position: relative; + margin: 2px; + position: relative; - >label { - position: absolute; - color:#c8c8c8; - font-size:8px; - padding:4px 4px 4px 4px; - top:0; - left:0; - right:15px; - } - >input, >textarea { - height:32px; - width:100%; - } - >input { - padding-top:12px; - } - >textarea { - padding-top:17px; - resize: vertical; - } - >label.opaque { - background-color:rgba(69,69,69,0.9); - border-radius: 2px; - } - &.disabled { - opacity: 0.5; - } - &.disabled>input { - border-color: transparent; - } -} -.input-block:focus-within>label { - color: #2196f3; + > label { + position: absolute; + color: #c8c8c8; + font-size: 8px; + padding: 4px 4px 4px 4px; + top: 0; + left: 0; + right: 15px; + } + > input, + > textarea { + height: 32px; + width: 100%; + } + > input { + padding-top: 12px; + } + > textarea { + padding-top: 17px; + resize: vertical; + } + > label.opaque { + background-color: rgba(69, 69, 69, 0.9); + border-radius: 2px; + } + &.disabled { + opacity: 0.5; + } + &.disabled > input { + border-color: transparent; + } +} +.input-block:focus-within > label { + color: #2196f3; } - .app-tooltip { - margin:1px !important; + margin: 1px !important; } .dialog .mat-dialog-container { - background-color: #252526; -} \ No newline at end of file + background-color: #252526; +} diff --git a/src/test.ts b/src/test.ts index 20423564..db91da99 100644 --- a/src/test.ts +++ b/src/test.ts @@ -1,14 +1,18 @@ // This file is required by karma.conf.js and loads recursively all the .spec and framework files import 'zone.js/testing'; -import { getTestBed } from '@angular/core/testing'; +import {getTestBed} from '@angular/core/testing'; import { BrowserDynamicTestingModule, - platformBrowserDynamicTesting + platformBrowserDynamicTesting, } from '@angular/platform-browser-dynamic/testing'; declare const require: { - context(path: string, deep?: boolean, filter?: RegExp): { + context( + path: string, + deep?: boolean, + filter?: RegExp + ): { keys(): string[]; (id: string): T; }; diff --git a/tsconfig.app.json b/tsconfig.app.json index 82d91dc4..ff396d4c 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -5,11 +5,6 @@ "outDir": "./out-tsc/app", "types": [] }, - "files": [ - "src/main.ts", - "src/polyfills.ts" - ], - "include": [ - "src/**/*.d.ts" - ] + "files": ["src/main.ts", "src/polyfills.ts"], + "include": ["src/**/*.d.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 6df82832..97d932eb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,10 +16,7 @@ "importHelpers": true, "target": "es2017", "module": "es2020", - "lib": [ - "es2018", - "dom" - ] + "lib": ["es2018", "dom"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 092345b0..669344f8 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -3,16 +3,8 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "./out-tsc/spec", - "types": [ - "jasmine" - ] + "types": ["jasmine"] }, - "files": [ - "src/test.ts", - "src/polyfills.ts" - ], - "include": [ - "src/**/*.spec.ts", - "src/**/*.d.ts" - ] + "files": ["src/test.ts", "src/polyfills.ts"], + "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] }