diff --git a/doc/screenshot.png b/doc/screenshot.png
index 4661409..b87185a 100644
Binary files a/doc/screenshot.png and b/doc/screenshot.png differ
diff --git a/src/app/app.component.html b/src/app/app.component.html
index c367ec4..b0dbe92 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -254,8 +254,7 @@
-
+
@@ -343,7 +342,8 @@
image
-
+
+
+
+
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index a8b9eb1..7db8a9c 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -10,6 +10,10 @@ import { UploadImageComponent } from './upload-image/upload-image.component';
import { ConfigService } from './config.service';
import { browserComputePathBoundingBox } from './svg-bbox';
+export const kDefaultPath = `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`;
+
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
@@ -32,10 +36,7 @@ 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 || kDefaultPath;
pathName: string = '';
invalidSyntax = false;
@@ -105,10 +106,16 @@ export class AppComponent implements AfterViewInit {
} 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)) {
- this.insert(key, this.focusedItem, false);
- $event.preventDefault();
+ if (isLower) {
+ // Item insertion
+ const lastItem = this.parsedPath.path.length ? this.parsedPath.path[this.parsedPath.path.length - 1] : null;
+ const prevItem = this.focusedItem || lastItem;
+ if(this.canInsertAfter(prevItem, key)) {
+ this.insert(key, prevItem, false);
+ $event.preventDefault();
+ }
} else if (!isLower && this.focusedItem && this.canConvert(this.focusedItem, key)) {
+ // Item convertion
this.insert(key, this.focusedItem, true);
$event.preventDefault();
}
@@ -221,11 +228,13 @@ export class AppComponent implements AfterViewInit {
this.strokeWidth = this.cfg.viewPortWidth / this.canvasWidth;
}
- insert(type: string, after: SvgItem, convert: boolean) {
+ insert(type: string, after: SvgItem | null, convert: boolean) {
if (convert) {
- this.focusedItem =
- this.parsedPath.changeType(after, after.relative ? type.toLowerCase() : type);
- this.afterModelChange();
+ if(after) {
+ this.focusedItem =
+ this.parsedPath.changeType(after, after.relative ? type.toLowerCase() : type);
+ this.afterModelChange();
+ }
} else {
this.draggedIsNew = true;
const pts = this.targetPoints;
@@ -263,7 +272,7 @@ export class AppComponent implements AfterViewInit {
newItem = SvgItem.Make([type]);
}
if(newItem) {
- this.parsedPath.insert(newItem, after);
+ this.parsedPath.insert(newItem, after ?? undefined);
}
}
this.setHistoryDisabled(true);
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 54b86bf..6414666 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -2,6 +2,8 @@ import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { ServiceWorkerModule } from '@angular/service-worker';
+import { HttpClientModule } from '@angular/common/http';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
@@ -9,27 +11,29 @@ 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 {MatSnackBarModule} from '@angular/material/snack-bar';
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 { Overlay, ScrollStrategy } from '@angular/cdk/overlay';
+import { ScrollingModule } from '@angular/cdk/scrolling';
+
+import { environment } from '../environments/environment';
+import { AppComponent } from './app.component';
import { FormatterDirective } from './formatter/formatter.directive';
import { KeyboardNavigableDirective } from './keyboard-navigable/keyboard-navigable.directive';
+import { ExpandableComponent } from './expandable/expandable.component';
+import { CanvasComponent } from './canvas/canvas.component';
+import { PathPreviewComponent } from './path-preview/path-preview.component';
+import { OpenComponent, OpenDialogComponent } from './open/open.component';
+import { SaveComponent, SaveDialogComponent } from './save/save.component';
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 { ImportComponent, ImportDialogComponent } from './import/import.component';
+import { ShareComponent, ShareDialogComponent, ShareDialogSnackbarComponent } from './share/share.component';
@NgModule({
declarations: [
@@ -44,8 +48,14 @@ import { environment } from '../environments/environment';
ExportDialogComponent,
UploadImageComponent,
UploadImageDialogComponent,
+ ImportComponent,
+ ImportDialogComponent,
+ ShareComponent,
+ ShareDialogComponent,
+ ShareDialogSnackbarComponent,
FormatterDirective,
- KeyboardNavigableDirective
+ KeyboardNavigableDirective,
+ PathPreviewComponent
],
imports: [
BrowserModule,
@@ -62,6 +72,7 @@ import { environment } from '../environments/environment';
MatTableModule,
MatSortModule,
MatSliderModule,
+ MatSnackBarModule,
BrowserAnimationsModule,
ScrollingModule,
ServiceWorkerModule.register('ngsw-worker.js', {
diff --git a/src/app/export/export-dialog.component.html b/src/app/export/export-dialog.component.html
index b457e2f..e0983c2 100644
--- a/src/app/export/export-dialog.component.html
+++ b/src/app/export/export-dialog.component.html
@@ -63,21 +63,17 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/src/app/export/export-dialog.component.scss b/src/app/export/export-dialog.component.scss
index 35f0eed..7aff0ac 100644
--- a/src/app/export/export-dialog.component.scss
+++ b/src/app/export/export-dialog.component.scss
@@ -43,7 +43,7 @@ mat-form-field {
}
.preview {
width:324px;
- svg {
+ app-path-preview {
margin:0 0 0 24px;
}
}
diff --git a/src/app/export/export.component.ts b/src/app/export/export.component.ts
index 3c8fef8..0ec72f5 100644
--- a/src/app/export/export.component.ts
+++ b/src/app/export/export.component.ts
@@ -54,10 +54,6 @@ export class ExportDialogComponent {
this.dialogRef.close();
}
- patternScale(containterWidth: number, containerHeight: number): number {
- return Math.max(this.width / containterWidth, this.height / containerHeight);
- }
-
refreshViewbox() {
const p = new Svg(this.data.path);
const locs = p.targetLocations();
diff --git a/src/app/import/import-dialog.component.html b/src/app/import/import-dialog.component.html
new file mode 100644
index 0000000..cf7c50d
--- /dev/null
+++ b/src/app/import/import-dialog.component.html
@@ -0,0 +1,20 @@
+Import shared path
+
+
+
+
+
+ Press Import to import this SVG path in the editor.
+
+
+ Please note it will erase any unsaved change.
+
+
+
+
+
+ Discard
+
+ Import
+
+
\ No newline at end of file
diff --git a/src/app/import/import.component.spec.ts b/src/app/import/import.component.spec.ts
new file mode 100644
index 0000000..8f82c81
--- /dev/null
+++ b/src/app/import/import.component.spec.ts
@@ -0,0 +1,28 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { MatDialogModule } from '@angular/material/dialog';
+import { MatIconModule } from '@angular/material/icon';
+
+import { ImportComponent } from './import.component';
+
+describe('ImportComponent', () => {
+ let component: ImportComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ MatDialogModule, MatIconModule ],
+ declarations: [ ImportComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ImportComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/import/import.component.ts b/src/app/import/import.component.ts
new file mode 100644
index 0000000..aea2d78
--- /dev/null
+++ b/src/app/import/import.component.ts
@@ -0,0 +1,89 @@
+import { Component, Output, EventEmitter, OnInit, Inject } from '@angular/core';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { kDefaultPath } from '../app.component';
+import { StorageService } from '../storage.service';
+import { Svg } from '../svg';
+
+export class DialogData {
+ path?: string;
+}
+
+@Component({
+ selector: 'app-import-dialog',
+ templateUrl: 'import-dialog.component.html'
+})
+export class ImportDialogComponent {
+ constructor(
+ public dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: DialogData
+ ) {
+ }
+
+ onConfirm(): void {
+ this.dialogRef.close(true);
+ }
+ onCancel(): void {
+ this.dialogRef.close();
+ }
+}
+
+@Component({
+ selector: 'app-import',
+ template: ''
+})
+export class ImportComponent implements OnInit {
+ private urlPath?: string;
+ @Output() importPath = new EventEmitter();
+
+ constructor(
+ public dialog: MatDialog,
+ public storageService: StorageService,
+ ) {
+ this.urlPath = this.readPath();
+ }
+
+ private readPath(): string {
+ const fragment = decodeURIComponent(window.location.hash.slice(1));
+ const check = /^P=[mMlLvVhHcCsSqQtTaAzZ0-9\-e._]+$/;
+ if(check.test(fragment)) {
+ const path = fragment.slice(2).replace(/_/g, ' ');
+ try {
+ const _ = new Svg(path);
+ return path;
+ } catch (e) {}
+ }
+ return '';
+ }
+
+ ngOnInit() {
+ const openedPath = this.storageService.getPath();
+ const unsavedChanges = openedPath && openedPath.path !== kDefaultPath;
+ if(this.urlPath && this.urlPath !== openedPath?.path) {
+ if(unsavedChanges) {
+ this.openDialog();
+ } else {
+ this.finalize(true);
+ }
+ }
+ }
+
+ openDialog(): void {
+ const dialogRef = this.dialog.open(ImportDialogComponent, {
+ width: '800px',
+ panelClass: 'dialog',
+ autoFocus: false,
+ data: {path: this.urlPath}
+ });
+
+ dialogRef.afterClosed().subscribe((result: boolean) => {
+ this.finalize(result);
+ });
+ }
+
+ private finalize(result: boolean): void {
+ if(result) {
+ this.importPath.emit(this.urlPath);
+ }
+ window.location.hash = '';
+ }
+}
diff --git a/src/app/path-preview/path-preview.component.html b/src/app/path-preview/path-preview.component.html
new file mode 100644
index 0000000..2c2fb1a
--- /dev/null
+++ b/src/app/path-preview/path-preview.component.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/path-preview/path-preview.component.spec.ts b/src/app/path-preview/path-preview.component.spec.ts
new file mode 100644
index 0000000..43a34e4
--- /dev/null
+++ b/src/app/path-preview/path-preview.component.spec.ts
@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PathPreviewComponent } from './path-preview.component';
+
+describe('PathPreviewComponent', () => {
+ let component: PathPreviewComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ PathPreviewComponent ]
+ })
+ .compileComponents();
+
+ fixture = TestBed.createComponent(PathPreviewComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/path-preview/path-preview.component.ts b/src/app/path-preview/path-preview.component.ts
new file mode 100644
index 0000000..4ed2cfc
--- /dev/null
+++ b/src/app/path-preview/path-preview.component.ts
@@ -0,0 +1,35 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { browserComputePathBoundingBox } from '../svg-bbox';
+
+@Component({
+ selector: 'app-path-preview',
+ templateUrl: './path-preview.component.html'
+})
+export class PathPreviewComponent implements OnInit {
+ @Input() x?: number;
+ @Input() y?: number;
+ @Input() width?: number;
+ @Input() height?: number;
+
+ @Input() fillColor?: string = '#000000';
+ @Input() strokeColor?: string;
+ @Input() strokeWidth?: number;
+ @Input() path = '';
+
+ constructor() { }
+
+ ngOnInit(): void {
+ if(this.x === undefined || this.y === undefined || this.width === undefined || this.height === undefined) {
+ const bbox = browserComputePathBoundingBox(this.path);
+ this.x = bbox.x;
+ this.y = bbox.y;
+ this.width = bbox.width;
+ this.height = bbox.height;
+ }
+ }
+
+ patternScale(containterWidth: number, containerHeight: number): number {
+ return Math.max((this.width??0) / containterWidth, (this.height??0) / containerHeight);
+ }
+
+}
diff --git a/src/app/share/share-dialog-snackbar.component.html b/src/app/share/share-dialog-snackbar.component.html
new file mode 100644
index 0000000..27b307d
--- /dev/null
+++ b/src/app/share/share-dialog-snackbar.component.html
@@ -0,0 +1,4 @@
+
+ Copied to clipboard
+ content_pasted
+
\ No newline at end of file
diff --git a/src/app/share/share-dialog.component.html b/src/app/share/share-dialog.component.html
new file mode 100644
index 0000000..3f1f531
--- /dev/null
+++ b/src/app/share/share-dialog.component.html
@@ -0,0 +1,19 @@
+Share path as URL
+
+
+
+
+
The current path can be shared using the following link:
+
+ URL
+
+
+ content_pasted
+
+
+
+
+
+
+ Close
+
\ No newline at end of file
diff --git a/src/app/share/share.component.html b/src/app/share/share.component.html
new file mode 100644
index 0000000..0d07549
--- /dev/null
+++ b/src/app/share/share.component.html
@@ -0,0 +1,8 @@
+
+ share
+
diff --git a/src/app/share/share.component.spec.ts b/src/app/share/share.component.spec.ts
new file mode 100644
index 0000000..4bf1acc
--- /dev/null
+++ b/src/app/share/share.component.spec.ts
@@ -0,0 +1,28 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { MatDialogModule } from '@angular/material/dialog';
+import { MatIconModule } from '@angular/material/icon';
+
+import { ShareComponent } from './share.component';
+
+describe('ShareComponent', () => {
+ let component: ShareComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [ MatDialogModule, MatIconModule ],
+ declarations: [ ShareComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ShareComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/src/app/share/share.component.ts b/src/app/share/share.component.ts
new file mode 100644
index 0000000..233c362
--- /dev/null
+++ b/src/app/share/share.component.ts
@@ -0,0 +1,84 @@
+import { Component, Output, EventEmitter, Inject, Input, ViewChild, AfterViewInit, ElementRef } from '@angular/core';
+import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { StorageService } from '../storage.service';
+
+export class DialogData {
+ path?: string;
+}
+
+@Component({
+ selector: 'app-share-snackbar',
+ templateUrl: 'share-dialog-snackbar.component.html'
+})
+export class ShareDialogSnackbarComponent {}
+
+
+@Component({
+ selector: 'app-share-dialog',
+ templateUrl: 'share-dialog.component.html'
+})
+export class ShareDialogComponent implements AfterViewInit {
+ @ViewChild('input') inputField?: ElementRef;
+
+ constructor(
+ public dialogRef: MatDialogRef,
+ @Inject(MAT_DIALOG_DATA) public data: DialogData,
+ private snackBar: MatSnackBar
+ ) {
+ }
+
+ ngAfterViewInit(): void {
+ setTimeout(() => this.selectText());
+ }
+
+ private selectText(): void {
+ const el = this.inputField?.nativeElement;
+ el?.focus();
+ el?.select();
+ }
+
+ onCancel(): void {
+ this.dialogRef.close();
+ }
+
+ copy(): void {
+ this.selectText();
+ navigator.clipboard.writeText(this.inputField?.nativeElement.value);
+ this.snackBar.openFromComponent(ShareDialogSnackbarComponent, {
+ horizontalPosition:'center',
+ verticalPosition: 'top',
+ duration: 2000
+ });
+ }
+
+ getUrl(): string {
+ const loc = window.location;
+ const fragment = this.data.path?.replace(/ +/g, '_');
+ return `${loc.protocol}//${loc.host}${loc.pathname}#P=${fragment}`;
+ }
+}
+
+@Component({
+ selector: 'app-share',
+ templateUrl: './share.component.html'
+})
+export class ShareComponent {
+ @Input() path: string = '';
+ @Output() importPath = new EventEmitter();
+
+ constructor(
+ public dialog: MatDialog,
+ public storageService: StorageService,
+ ) {
+ }
+
+ openDialog(): void {
+ const dialogRef = this.dialog.open(ShareDialogComponent, {
+ width: '800px',
+ panelClass: 'dialog',
+ autoFocus: false,
+ data: {path: this.path}
+ });
+ }
+}
diff --git a/src/styles.scss b/src/styles.scss
index b5ea762..43d6202 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -107,7 +107,7 @@ button.mat-flat-button {
font-size: 12px;
}
-button.mat-icon-button {
+button.mat-icon-button.more-button {
width:20px;
height:20px;
line-height: 20px !important;