From 9a826665d2c9b7ff5048ca0103d5ce21aaa4cad1 Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Sun, 10 Mar 2024 12:28:43 +0300 Subject: [PATCH 01/22] package: perfect-freehand --- package.json | 1 + yarn.lock | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index b3f739a19..2f5d49f9b 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ "openapi-fetch": "^0.8.2", "papaparse": "^5.3.2", "parse": "3.4.2", + "perfect-freehand": "^1.2.2", "rxdb": "^14.11.1", "rxjs": "~7.8.0", "save-svg-as-png": "^1.4.17", diff --git a/yarn.lock b/yarn.lock index aa8e160ae..531ae5131 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15147,7 +15147,7 @@ __metadata: "leaflet-draw@github:enketo/Leaflet.draw#ff730785db7fcccbf2485ffcf4dffe1238a7c617": version: 1.0.4 resolution: "leaflet-draw@https://github.com/enketo/Leaflet.draw.git#commit=ff730785db7fcccbf2485ffcf4dffe1238a7c617" - checksum: b08b88994769667f11f2b6a8937656c89cea34dafd4661abab0b48b4b97f3bddbdce7b23ddfdb8d7c6335e065530e32a70e281314afa34afa134bf68597945fc + checksum: b2280f200f5ea410e33cc5feb500d67d86cb5f16b294eea1306da055614ffc906be7de6dbc9bbf4db6e6e5d27d0396e4701a3b6c4c920548d2aac94b8223879f languageName: node linkType: hard @@ -17475,6 +17475,13 @@ __metadata: languageName: node linkType: hard +"perfect-freehand@npm:^1.2.2": + version: 1.2.2 + resolution: "perfect-freehand@npm:1.2.2" + checksum: e1c4f3b6c9e61a3ba4a5375d7cbe05d88fb4885b2bbd6c7dd38437c25ce2e5af1102d46c9b8ccfd1e4f5cf9c7855605f8e6cda073157f9bf68beea85bd98d48b + languageName: node + linkType: hard + "performance-now@npm:^2.1.0": version: 2.1.0 resolution: "performance-now@npm:2.1.0" @@ -17640,6 +17647,7 @@ __metadata: openapi-typescript: ^6.7.3 papaparse: ^5.3.2 parse: 3.4.2 + perfect-freehand: ^1.2.2 postcss: ^8.4.5 postcss-import: ~14.1.0 postcss-url: ~10.1.3 From bb846f94da5abb89ad79248ad18fbaa0cbfd5c37 Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Mon, 11 Mar 2024 08:59:43 +0300 Subject: [PATCH 02/22] feat: drawing component --- .../features/drawing/drawing.component.html | 1 + .../features/drawing/drawing.component.scss | 0 .../drawing/drawing.component.spec.ts | 21 +++++++++++++++++++ .../src/features/drawing/drawing.component.ts | 12 +++++++++++ 4 files changed, 34 insertions(+) create mode 100644 libs/shared/src/features/drawing/drawing.component.html create mode 100644 libs/shared/src/features/drawing/drawing.component.scss create mode 100644 libs/shared/src/features/drawing/drawing.component.spec.ts create mode 100644 libs/shared/src/features/drawing/drawing.component.ts diff --git a/libs/shared/src/features/drawing/drawing.component.html b/libs/shared/src/features/drawing/drawing.component.html new file mode 100644 index 000000000..3e8fcacea --- /dev/null +++ b/libs/shared/src/features/drawing/drawing.component.html @@ -0,0 +1 @@ + diff --git a/libs/shared/src/features/drawing/drawing.component.scss b/libs/shared/src/features/drawing/drawing.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/libs/shared/src/features/drawing/drawing.component.spec.ts b/libs/shared/src/features/drawing/drawing.component.spec.ts new file mode 100644 index 000000000..4ceb8ced4 --- /dev/null +++ b/libs/shared/src/features/drawing/drawing.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DrawingComponent } from './drawing.component'; + +describe('DrawingComponent', () => { + let component: DrawingComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DrawingComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DrawingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/libs/shared/src/features/drawing/drawing.component.ts b/libs/shared/src/features/drawing/drawing.component.ts new file mode 100644 index 000000000..3f86bb60e --- /dev/null +++ b/libs/shared/src/features/drawing/drawing.component.ts @@ -0,0 +1,12 @@ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import {getStroke} from 'perfect-freehand' + +@Component({ + selector: 'picsa-custom-drawing', + standalone: true, + imports: [CommonModule], + templateUrl: './drawing.component.html', + styleUrls: ['./drawing.component.scss'], +}) +export class DrawingComponent {} From f016624b0544c99c23aad2ece3f525bf91656d4e Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Mon, 22 Apr 2024 08:12:02 +0300 Subject: [PATCH 03/22] feat: drawing methods --- .../src/features/drawing/drawing.component.ts | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/libs/shared/src/features/drawing/drawing.component.ts b/libs/shared/src/features/drawing/drawing.component.ts index 3f86bb60e..758fa092d 100644 --- a/libs/shared/src/features/drawing/drawing.component.ts +++ b/libs/shared/src/features/drawing/drawing.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import {getStroke} from 'perfect-freehand' +import { getStroke } from 'perfect-freehand'; @Component({ selector: 'picsa-custom-drawing', @@ -9,4 +9,38 @@ import {getStroke} from 'perfect-freehand' templateUrl: './drawing.component.html', styleUrls: ['./drawing.component.scss'], }) -export class DrawingComponent {} +export class DrawingComponent { + options = { + size: 32, + thinning: 0.5, + smoothing: 0.5, + streamline: 0.5, + easing: (t) => t, + start: { + taper: 0, + easing: (t) => t, + cap: true, + }, + end: { + taper: 100, + easing: (t) => t, + cap: true, + }, + }; + + getSvgPathFromStroke(stroke) { + if (!stroke.length) return ''; + + const d = stroke.reduce( + (acc, [x0, y0], i, arr) => { + const [x1, y1] = arr[(i + 1) % arr.length]; + acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2); + return acc; + }, + ['M', ...stroke[0], 'Q'] + ); + + d.push('Z'); + return d.join(' '); + } +} From b47fa981b212647eef302fb8e45c5171fdae97fb Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Mon, 22 Apr 2024 10:35:18 +0300 Subject: [PATCH 04/22] fix: import declaration linting and component names --- .../data-table/data-table.component.spec.ts | 13 +++++++------ .../src/features/drawing/drawing.component.spec.ts | 11 ++++++----- .../video-player/video-player.component.spec.ts | 1 + .../storage-file-picker.component.spec.ts | 13 +++++++------ .../upload/supabase-upload.component.spec.ts | 1 + 5 files changed, 22 insertions(+), 17 deletions(-) diff --git a/libs/shared/src/features/data-table/data-table.component.spec.ts b/libs/shared/src/features/data-table/data-table.component.spec.ts index 58e1caa18..840288dfe 100644 --- a/libs/shared/src/features/data-table/data-table.component.spec.ts +++ b/libs/shared/src/features/data-table/data-table.component.spec.ts @@ -1,16 +1,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DataTableComponent } from './data-table.component'; -describe('DataTableComponent', () => { - let component: DataTableComponent; - let fixture: ComponentFixture; +import { PicsaDataTableComponent } from './data-table.component'; + +describe('PicsaDataTableComponent', () => { + let component: PicsaDataTableComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DataTableComponent], + imports: [PicsaDataTableComponent], }).compileComponents(); - fixture = TestBed.createComponent(DataTableComponent); + fixture = TestBed.createComponent(PicsaDataTableComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/libs/shared/src/features/drawing/drawing.component.spec.ts b/libs/shared/src/features/drawing/drawing.component.spec.ts index 4ceb8ced4..94eb7c7f4 100644 --- a/libs/shared/src/features/drawing/drawing.component.spec.ts +++ b/libs/shared/src/features/drawing/drawing.component.spec.ts @@ -1,16 +1,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { DrawingComponent } from './drawing.component'; + +import { PicsaDrawingComponent } from './drawing.component'; describe('DrawingComponent', () => { - let component: DrawingComponent; - let fixture: ComponentFixture; + let component: PicsaDrawingComponent; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DrawingComponent], + imports: [PicsaDrawingComponent], }).compileComponents(); - fixture = TestBed.createComponent(DrawingComponent); + fixture = TestBed.createComponent(PicsaDrawingComponent); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/libs/shared/src/features/video-player/video-player.component.spec.ts b/libs/shared/src/features/video-player/video-player.component.spec.ts index 117bbba91..f9672fd50 100644 --- a/libs/shared/src/features/video-player/video-player.component.spec.ts +++ b/libs/shared/src/features/video-player/video-player.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; + import { VideoPlayerComponent } from './video-player.component'; describe('VideoPlayerComponent', () => { diff --git a/libs/shared/src/services/core/supabase/components/storage-file-picker/storage-file-picker.component.spec.ts b/libs/shared/src/services/core/supabase/components/storage-file-picker/storage-file-picker.component.spec.ts index 85ff16cbe..12c4b392b 100644 --- a/libs/shared/src/services/core/supabase/components/storage-file-picker/storage-file-picker.component.spec.ts +++ b/libs/shared/src/services/core/supabase/components/storage-file-picker/storage-file-picker.component.spec.ts @@ -1,16 +1,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { StorageFilePickerComponent } from './storage-file-picker.component'; -describe('StorageFilePickerComponent', () => { - let component: StorageFilePickerComponent; - let fixture: ComponentFixture; +import { SupabaseStoragePickerDirective } from './storage-file-picker.component'; + +describe('SupabaseStoragePickerDirective', () => { + let component: SupabaseStoragePickerDirective; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [StorageFilePickerComponent], + imports: [SupabaseStoragePickerDirective], }).compileComponents(); - fixture = TestBed.createComponent(StorageFilePickerComponent); + fixture = TestBed.createComponent(SupabaseStoragePickerDirective); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/libs/shared/src/services/core/supabase/components/upload/supabase-upload.component.spec.ts b/libs/shared/src/services/core/supabase/components/upload/supabase-upload.component.spec.ts index e4925cf7c..5a35f6c7b 100644 --- a/libs/shared/src/services/core/supabase/components/upload/supabase-upload.component.spec.ts +++ b/libs/shared/src/services/core/supabase/components/upload/supabase-upload.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; + import { SupabaseUploadComponent } from './supabase-upload.component'; describe('SupabaseUploadComponent', () => { From 06abc8b20837168f369d2c767c284927579e2af5 Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Mon, 13 May 2024 08:12:27 +0300 Subject: [PATCH 05/22] feta: Drawing shared Component --- .../features/drawing/drawing.component.html | 6 +++- .../features/drawing/drawing.component.scss | 8 ++++++ .../src/features/drawing/drawing.component.ts | 28 +++++++++++++++++-- libs/shared/src/features/drawing/index.ts | 1 + libs/shared/src/features/index.ts | 3 +- 5 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 libs/shared/src/features/drawing/index.ts diff --git a/libs/shared/src/features/drawing/drawing.component.html b/libs/shared/src/features/drawing/drawing.component.html index 3e8fcacea..4d7fe32d0 100644 --- a/libs/shared/src/features/drawing/drawing.component.html +++ b/libs/shared/src/features/drawing/drawing.component.html @@ -1 +1,5 @@ - +
+ + + +
diff --git a/libs/shared/src/features/drawing/drawing.component.scss b/libs/shared/src/features/drawing/drawing.component.scss index e69de29bb..3494899e0 100644 --- a/libs/shared/src/features/drawing/drawing.component.scss +++ b/libs/shared/src/features/drawing/drawing.component.scss @@ -0,0 +1,8 @@ +.svg-container{ + border: 1px solid #eeeeee; + border-radius: 4px; +} +svg.svg{ + width: 100%; + touch-action: none; +} \ No newline at end of file diff --git a/libs/shared/src/features/drawing/drawing.component.ts b/libs/shared/src/features/drawing/drawing.component.ts index 758fa092d..a2a187a96 100644 --- a/libs/shared/src/features/drawing/drawing.component.ts +++ b/libs/shared/src/features/drawing/drawing.component.ts @@ -9,7 +9,7 @@ import { getStroke } from 'perfect-freehand'; templateUrl: './drawing.component.html', styleUrls: ['./drawing.component.scss'], }) -export class DrawingComponent { +export class PicsaDrawingComponent { options = { size: 32, thinning: 0.5, @@ -27,8 +27,17 @@ export class DrawingComponent { cap: true, }, }; + public points: any[] = []; + public stroke; + public pathData; - getSvgPathFromStroke(stroke) { + constructor() { + this.stroke = getStroke(this.points, this.options); + this.pathData = this.getSvgPathFromStroke(this.stroke); + } + + public getSvgPathFromStroke(stroke) { + console.log('This is the stroke received:', stroke); if (!stroke.length) return ''; const d = stroke.reduce( @@ -41,6 +50,21 @@ export class DrawingComponent { ); d.push('Z'); + console.log('Path:', d); return d.join(' '); } + + handlePointerDown(event) { + event.target.setPointerCapture(event.pointerId); + this.points = [[event.pageX, event.pageY, 0.5]]; + } + + handlePointerMove(event) { + if (event.buttons !== 1) return; + this.points = [...this.points, [event.pageX, event.pageY, 0.5]]; + console.log('Points:', this.points); + this.stroke = getStroke(this.points, this.options); + console.log('Stroke:', this.stroke); + this.pathData = this.getSvgPathFromStroke(this.stroke); + } } diff --git a/libs/shared/src/features/drawing/index.ts b/libs/shared/src/features/drawing/index.ts new file mode 100644 index 000000000..8c0ec4b6b --- /dev/null +++ b/libs/shared/src/features/drawing/index.ts @@ -0,0 +1 @@ +export * from './drawing.component' \ No newline at end of file diff --git a/libs/shared/src/features/index.ts b/libs/shared/src/features/index.ts index 9adf9998a..5fdc7d619 100644 --- a/libs/shared/src/features/index.ts +++ b/libs/shared/src/features/index.ts @@ -2,4 +2,5 @@ export * from './animations'; export * from './charts'; export * from './data-table'; export * from './dialog'; -export * from './video-player'; +export * from './drawing'; +export * from './video-player'; \ No newline at end of file From 4158ae48652340be19daaff9f889105b6a978e5b Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Mon, 13 May 2024 08:13:59 +0300 Subject: [PATCH 06/22] feat: Budget tool drawing component --- .../budget-draw/budget-draw.component.html | 1 + .../budget-draw/budget-draw.component.scss | 0 .../budget-draw/budget-draw.component.spec.ts | 21 +++++++++++++++++++ .../budget-draw/budget-draw.component.ts | 11 ++++++++++ 4 files changed, 33 insertions(+) create mode 100644 apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.html create mode 100644 apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.scss create mode 100644 apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.spec.ts create mode 100644 apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.ts diff --git a/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.html b/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.html new file mode 100644 index 000000000..c232790aa --- /dev/null +++ b/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.html @@ -0,0 +1 @@ + diff --git a/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.scss b/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.spec.ts b/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.spec.ts new file mode 100644 index 000000000..82bbe8981 --- /dev/null +++ b/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { BudgetDrawComponent } from './budget-draw.component'; + +describe('BudgetDrawComponent', () => { + let component: BudgetDrawComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [BudgetDrawComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(BudgetDrawComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.ts b/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.ts new file mode 100644 index 000000000..0126f1bd6 --- /dev/null +++ b/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { PicsaDrawingComponent } from '@picsa/shared/features'; + +@Component({ + selector: 'budget-draw', + standalone:true, + imports: [PicsaDrawingComponent], + templateUrl: './budget-draw.component.html', + styleUrl: './budget-draw.component.scss', +}) +export class BudgetDrawComponent {} From 7b0a5fb7aa9eb1c2b3d50eb79c3fa484af07be6c Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Mon, 13 May 2024 08:14:33 +0300 Subject: [PATCH 07/22] feat: drawing component configurations --- .../src/app/components/budget-tool.components.ts | 4 +++- .../budget-tool/src/app/pages/home/budget-home.module.ts | 2 ++ .../budget-tool/src/app/pages/home/budget-home.page.html | 7 ++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/picsa-tools/budget-tool/src/app/components/budget-tool.components.ts b/apps/picsa-tools/budget-tool/src/app/components/budget-tool.components.ts index 97df18330..4dac401f1 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/budget-tool.components.ts +++ b/apps/picsa-tools/budget-tool/src/app/components/budget-tool.components.ts @@ -4,7 +4,7 @@ import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { PicsaCommonComponentsModule } from '@picsa/components'; -import { PicsaDialogsModule } from '@picsa/shared/features'; +import { PicsaDialogsModule, PicsaDrawingComponent } from '@picsa/shared/features'; import { PicsaDbModule } from '@picsa/shared/modules'; import { MobxAngularModule } from 'mobx-angular'; import { MatTooltipModule } from '@angular/material/tooltip'; @@ -32,6 +32,7 @@ import { BudgetPeriodSummaryComponent } from './summary/period-summary'; import { BudgetTableComponent } from './table/budget-table'; import { BudgetSummaryComponent } from './budget-summary/budget-summary.component'; import { MatIconModule } from '@angular/material/icon'; +import { BudgetDrawComponent } from './budget-draw/budget-draw.component'; const components = [ BudgetBalanceLegendComponent, @@ -71,6 +72,7 @@ const components = [ RouterModule, MatTooltipModule, MatIconModule, + PicsaDrawingComponent ], exports: components, }) diff --git a/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.module.ts b/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.module.ts index a772307cd..f90030eb0 100644 --- a/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.module.ts +++ b/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.module.ts @@ -9,6 +9,7 @@ import { MobxAngularModule } from 'mobx-angular'; import { BudgetToolComponentsModule } from '../../components/budget-tool.components'; import { BudgetMaterialModule } from '../../material.module'; import { BudgetHomePage } from './budget-home.page'; +import { BudgetDrawComponent } from '../../components/budget-draw/budget-draw.component'; const routes: Routes = [ { @@ -27,6 +28,7 @@ const routes: Routes = [ BudgetMaterialModule, BudgetToolComponentsModule, MobxAngularModule, + BudgetDrawComponent ], declarations: [BudgetHomePage], }) diff --git a/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.page.html b/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.page.html index 0d8a17753..53a431a97 100644 --- a/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.page.html +++ b/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.page.html @@ -9,7 +9,12 @@ - + +
+

Draw here!

+ +
+

{{ 'Saved Budgets' | translate }}

Date: Mon, 13 May 2024 22:09:06 +0100 Subject: [PATCH 08/22] fix: custom drawing render --- .../features/drawing/drawing.component.html | 23 ++++-- .../features/drawing/drawing.component.scss | 14 ++-- .../src/features/drawing/drawing.component.ts | 75 ++++++++++++------- yarn.lock | 2 +- 4 files changed, 74 insertions(+), 40 deletions(-) diff --git a/libs/shared/src/features/drawing/drawing.component.html b/libs/shared/src/features/drawing/drawing.component.html index 4d7fe32d0..bd17298c9 100644 --- a/libs/shared/src/features/drawing/drawing.component.html +++ b/libs/shared/src/features/drawing/drawing.component.html @@ -1,5 +1,18 @@ -
- - - -
+ + + +
+ + @if(points){ + + } + +
+
diff --git a/libs/shared/src/features/drawing/drawing.component.scss b/libs/shared/src/features/drawing/drawing.component.scss index 3494899e0..2595e9e99 100644 --- a/libs/shared/src/features/drawing/drawing.component.scss +++ b/libs/shared/src/features/drawing/drawing.component.scss @@ -1,8 +1,8 @@ -.svg-container{ - border: 1px solid #eeeeee; - border-radius: 4px; +.svg-dialog-container { + border: 1px solid #eeeeee; + border-radius: 4px; + box-sizing: border-box; + // mat-dialog has max-width 80vw and includes 24px padding so just fit max + width: calc(80vw - 48px); + height: 80vh; } -svg.svg{ - width: 100%; - touch-action: none; -} \ No newline at end of file diff --git a/libs/shared/src/features/drawing/drawing.component.ts b/libs/shared/src/features/drawing/drawing.component.ts index a2a187a96..349124278 100644 --- a/libs/shared/src/features/drawing/drawing.component.ts +++ b/libs/shared/src/features/drawing/drawing.component.ts @@ -1,11 +1,13 @@ import { CommonModule } from '@angular/common'; -import { Component } from '@angular/core'; +import { Component, ElementRef, signal, ViewChild } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { getStroke } from 'perfect-freehand'; @Component({ selector: 'picsa-custom-drawing', standalone: true, - imports: [CommonModule], + imports: [CommonModule, MatDialogModule, MatButtonModule], templateUrl: './drawing.component.html', styleUrls: ['./drawing.component.scss'], }) @@ -27,19 +29,54 @@ export class PicsaDrawingComponent { cap: true, }, }; - public points: any[] = []; - public stroke; - public pathData; + /** [x,y,pressure] point representation */ + public points: [number, number, number][] = []; - constructor() { - this.stroke = getStroke(this.points, this.options); - this.pathData = this.getSvgPathFromStroke(this.stroke); + /** SVG representation of active path segment */ + public pathData = signal(''); + + /** + * When the svg is rendered in a parent element track position + * to use to offset + */ + private containerOffset = [0, 0]; + + @ViewChild('svgElement') svgElement: ElementRef; + + constructor(public dialog: MatDialog) {} + + handlePointerDown(event: PointerEvent) { + const target = event.target as SVGElement; + target.setPointerCapture(event.pointerId); + this.calculateContainerOffset(); + this.addPointToPath(event.pageX, event.pageY); } - public getSvgPathFromStroke(stroke) { - console.log('This is the stroke received:', stroke); - if (!stroke.length) return ''; + handlePointerMove(event: PointerEvent) { + if (event.buttons !== 1) return; + this.addPointToPath(event.pageX, event.pageY); + } + + private addPointToPath(x: number, y: number, pressure = 0.5) { + const [left, top] = this.containerOffset; + this.points.push([x - left, y - top, pressure]); + const stroke = getStroke(this.points); + const svgPath = this.getSvgPathFromStroke(stroke); + this.pathData.set(svgPath); + } + private calculateContainerOffset() { + const svgEl = this.svgElement.nativeElement; + const { left, top } = svgEl.getBoundingClientRect(); + this.containerOffset = [left, top]; + } + + /** + * Generate an svg path from array of [x,y] point arrays + * Copied from https://github.com/steveruizok/perfect-freehand + * */ + private getSvgPathFromStroke(stroke: number[][]) { + if (!stroke.length) return ''; const d = stroke.reduce( (acc, [x0, y0], i, arr) => { const [x1, y1] = arr[(i + 1) % arr.length]; @@ -48,23 +85,7 @@ export class PicsaDrawingComponent { }, ['M', ...stroke[0], 'Q'] ); - d.push('Z'); - console.log('Path:', d); return d.join(' '); } - - handlePointerDown(event) { - event.target.setPointerCapture(event.pointerId); - this.points = [[event.pageX, event.pageY, 0.5]]; - } - - handlePointerMove(event) { - if (event.buttons !== 1) return; - this.points = [...this.points, [event.pageX, event.pageY, 0.5]]; - console.log('Points:', this.points); - this.stroke = getStroke(this.points, this.options); - console.log('Stroke:', this.stroke); - this.pathData = this.getSvgPathFromStroke(this.stroke); - } } diff --git a/yarn.lock b/yarn.lock index 5f2cddecd..8bfe58e72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16900,7 +16900,7 @@ __metadata: "leaflet-draw@github:enketo/Leaflet.draw#ff730785db7fcccbf2485ffcf4dffe1238a7c617": version: 1.0.4 resolution: "leaflet-draw@https://github.com/enketo/Leaflet.draw.git#commit=ff730785db7fcccbf2485ffcf4dffe1238a7c617" - checksum: b2280f200f5ea410e33cc5feb500d67d86cb5f16b294eea1306da055614ffc906be7de6dbc9bbf4db6e6e5d27d0396e4701a3b6c4c920548d2aac94b8223879f + checksum: b08b88994769667f11f2b6a8937656c89cea34dafd4661abab0b48b4b97f3bddbdce7b23ddfdb8d7c6335e065530e32a70e281314afa34afa134bf68597945fc languageName: node linkType: hard From ffa566b4b3ab7cf26381b80043f2a8a9a72fd97f Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 13 May 2024 22:41:58 +0100 Subject: [PATCH 09/22] chore: code tidying --- .../src/features/drawing/drawing.component.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/libs/shared/src/features/drawing/drawing.component.ts b/libs/shared/src/features/drawing/drawing.component.ts index 349124278..e766d7af6 100644 --- a/libs/shared/src/features/drawing/drawing.component.ts +++ b/libs/shared/src/features/drawing/drawing.component.ts @@ -50,27 +50,35 @@ export class PicsaDrawingComponent { target.setPointerCapture(event.pointerId); this.calculateContainerOffset(); this.addPointToPath(event.pageX, event.pageY); + this.renderPath(); } handlePointerMove(event: PointerEvent) { if (event.buttons !== 1) return; this.addPointToPath(event.pageX, event.pageY); + this.renderPath(); } + /** Add a point to the current path, adjusting absolute position for relative container */ private addPointToPath(x: number, y: number, pressure = 0.5) { const [left, top] = this.containerOffset; this.points.push([x - left, y - top, pressure]); - const stroke = getStroke(this.points); - const svgPath = this.getSvgPathFromStroke(stroke); - this.pathData.set(svgPath); } + /** Determine current positioning of svg drawing container to use for path offsets */ private calculateContainerOffset() { const svgEl = this.svgElement.nativeElement; const { left, top } = svgEl.getBoundingClientRect(); this.containerOffset = [left, top]; } + /** Render an svg path element generated from current list of points */ + private renderPath() { + const stroke = getStroke(this.points); + const svgPath = this.getSvgPathFromStroke(stroke); + this.pathData.set(svgPath); + } + /** * Generate an svg path from array of [x,y] point arrays * Copied from https://github.com/steveruizok/perfect-freehand From 483a1da9b7e9d27ec1d096ae7f4083827d1829ed Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Mon, 27 May 2024 09:22:41 +0300 Subject: [PATCH 10/22] feat: undo, redo and clear draw interactions --- libs/shared/src/features/drawing/drawing.component.html | 8 +++++++- libs/shared/src/features/drawing/drawing.component.scss | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/libs/shared/src/features/drawing/drawing.component.html b/libs/shared/src/features/drawing/drawing.component.html index bd17298c9..423686c83 100644 --- a/libs/shared/src/features/drawing/drawing.component.html +++ b/libs/shared/src/features/drawing/drawing.component.html @@ -1,6 +1,10 @@ - + +
+ + +
@if(points){ } +
diff --git a/libs/shared/src/features/drawing/drawing.component.scss b/libs/shared/src/features/drawing/drawing.component.scss index 2595e9e99..ef13d4e85 100644 --- a/libs/shared/src/features/drawing/drawing.component.scss +++ b/libs/shared/src/features/drawing/drawing.component.scss @@ -6,3 +6,9 @@ width: calc(80vw - 48px); height: 80vh; } + +.flex.flex-row{ + display: flex; + flex-direction: row; + justify-content: flex-start; +} From 2cc0b298a27d6356837eb804e5e1ac3e0b3e6a50 Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Mon, 27 May 2024 09:50:57 +0300 Subject: [PATCH 11/22] feat: undo, redo and clear draw functions --- .../src/features/drawing/drawing.component.ts | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/libs/shared/src/features/drawing/drawing.component.ts b/libs/shared/src/features/drawing/drawing.component.ts index e766d7af6..7b8dfd2cf 100644 --- a/libs/shared/src/features/drawing/drawing.component.ts +++ b/libs/shared/src/features/drawing/drawing.component.ts @@ -1,5 +1,6 @@ import { CommonModule } from '@angular/common'; import { Component, ElementRef, signal, ViewChild } from '@angular/core'; +import { UntypedFormBuilder } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { getStroke } from 'perfect-freehand'; @@ -24,7 +25,7 @@ export class PicsaDrawingComponent { cap: true, }, end: { - taper: 100, + taper: 0, easing: (t) => t, cap: true, }, @@ -35,6 +36,10 @@ export class PicsaDrawingComponent { /** SVG representation of active path segment */ public pathData = signal(''); + private undoStack: [number, number, number][][] = []; + + private redoStack: [number, number, number][][] = []; + /** * When the svg is rendered in a parent element track position * to use to offset @@ -59,6 +64,11 @@ export class PicsaDrawingComponent { this.renderPath(); } + handlePointerUp(event: PointerEvent) { + this._pushToUndo(); + this._endCurrentStroke(event); + } + /** Add a point to the current path, adjusting absolute position for relative container */ private addPointToPath(x: number, y: number, pressure = 0.5) { const [left, top] = this.containerOffset; @@ -77,6 +87,7 @@ export class PicsaDrawingComponent { const stroke = getStroke(this.points); const svgPath = this.getSvgPathFromStroke(stroke); this.pathData.set(svgPath); + this._pushToUndo(); } /** @@ -96,4 +107,61 @@ export class PicsaDrawingComponent { d.push('Z'); return d.join(' '); } + + /* Push to Undo Stack */ + private _pushToUndo() { + this.undoStack.push([...this.points]); + } + + /* Clear Draw */ + clearDraw() { + this.points = []; + this.renderPath(); + } + + /* Undo previous Stroke render */ + public undoSvgStroke() { + if (this.undoStack.length) { + const currentState = this.undoStack.pop(); + if (currentState) { + this.redoStack.push(currentState); + this.points = currentState; + const stroke = getStroke(this.points); + const svgPath = this.getSvgPathFromStroke(stroke); + this.pathData.set(svgPath); + } + } else { + return; + } + } + + /* Redo Stroke render */ + public redoSvgStroke() { + if (this.redoStack.length) { + const currentState = this.redoStack.pop(); + if (currentState) { + this.undoStack.push(currentState); + this.points = currentState; + const stroke = getStroke(this.points); + const svgPath = this.getSvgPathFromStroke(stroke); + this.pathData.set(svgPath); + } + } else { + return; + } + } + + public _startNewStroke() { + if (this.points.length > 0) { + this._pushToUndo(); // Save the current stroke before starting a new one + } + this.points = []; // Clear the points for the new stroke + } + + // Method to end the current stroke + public _endCurrentStroke(event: PointerEvent) { + // this.addPointToPath(event.pageX, event.pageY); + // this.points = null as never + // this.points.push([0, 0, 0]) + } } From 5ee2172561219667f6c934b23b8946d5cb56eb69 Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Tue, 28 May 2024 20:00:04 +0300 Subject: [PATCH 12/22] update html template --- libs/shared/src/features/drawing/drawing.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/shared/src/features/drawing/drawing.component.html b/libs/shared/src/features/drawing/drawing.component.html index 423686c83..0b09b78e8 100644 --- a/libs/shared/src/features/drawing/drawing.component.html +++ b/libs/shared/src/features/drawing/drawing.component.html @@ -12,13 +12,13 @@ height="100%" (pointerdown)="handlePointerDown($event)" (pointermove)="handlePointerMove($event)" - (pointerup)="handlePointerUp($event)" + (pointerup)="handlePointerUp()" style="touch-action: none" > @if(points){ } - +
From a6d4f859f1f0120f7de4c7717e13a7fe40234ec6 Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Tue, 28 May 2024 20:00:52 +0300 Subject: [PATCH 13/22] fix svg path breaks --- .../src/features/drawing/drawing.component.ts | 91 +++++++++++-------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/libs/shared/src/features/drawing/drawing.component.ts b/libs/shared/src/features/drawing/drawing.component.ts index 7b8dfd2cf..33024ca61 100644 --- a/libs/shared/src/features/drawing/drawing.component.ts +++ b/libs/shared/src/features/drawing/drawing.component.ts @@ -1,10 +1,11 @@ import { CommonModule } from '@angular/common'; import { Component, ElementRef, signal, ViewChild } from '@angular/core'; -import { UntypedFormBuilder } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { getStroke } from 'perfect-freehand'; +type Point = [number, number, number]; +type Segment = Point[]; @Component({ selector: 'picsa-custom-drawing', standalone: true, @@ -30,16 +31,16 @@ export class PicsaDrawingComponent { cap: true, }, }; + /** [x,y,pressure] point representation */ - public points: [number, number, number][] = []; + public points: Segment = []; + public segments: Segment[] = []; + public undoStack: Segment[][] = []; + public redoStack: Segment[][] = []; /** SVG representation of active path segment */ public pathData = signal(''); - private undoStack: [number, number, number][][] = []; - - private redoStack: [number, number, number][][] = []; - /** * When the svg is rendered in a parent element track position * to use to offset @@ -62,17 +63,18 @@ export class PicsaDrawingComponent { if (event.buttons !== 1) return; this.addPointToPath(event.pageX, event.pageY); this.renderPath(); + this._pushToUndo(); } - handlePointerUp(event: PointerEvent) { - this._pushToUndo(); - this._endCurrentStroke(event); + handlePointerUp() { + this._endCurrentStroke(); } /** Add a point to the current path, adjusting absolute position for relative container */ private addPointToPath(x: number, y: number, pressure = 0.5) { const [left, top] = this.containerOffset; this.points.push([x - left, y - top, pressure]); + this.segments.push(this.points); } /** Determine current positioning of svg drawing container to use for path offsets */ @@ -84,10 +86,8 @@ export class PicsaDrawingComponent { /** Render an svg path element generated from current list of points */ private renderPath() { - const stroke = getStroke(this.points); - const svgPath = this.getSvgPathFromStroke(stroke); - this.pathData.set(svgPath); - this._pushToUndo(); + const allPaths = this.segments.flatMap((segment) => this.getSvgPathFromStroke(getStroke(segment))).join(' '); + this.pathData.set(allPaths); } /** @@ -110,28 +110,47 @@ export class PicsaDrawingComponent { /* Push to Undo Stack */ private _pushToUndo() { - this.undoStack.push([...this.points]); + if (this.points.length > 0) { + this.undoStack.push([...this.segments]); + this.redoStack = []; + } } /* Clear Draw */ - clearDraw() { + public clearDraw() { + this.segments = []; this.points = []; this.renderPath(); + this._pushToUndo(); } /* Undo previous Stroke render */ public undoSvgStroke() { - if (this.undoStack.length) { - const currentState = this.undoStack.pop(); - if (currentState) { - this.redoStack.push(currentState); - this.points = currentState; - const stroke = getStroke(this.points); - const svgPath = this.getSvgPathFromStroke(stroke); - this.pathData.set(svgPath); + if (this.undoStack.length > 0) { + // const currentState = this.undoStack.pop(); + // this.redoStack.push(currentState as Segment[]); + // if (currentState && currentState.length > 0) { + // this.segments = currentState; + // const allPaths = this.segments.map((segment) => this.getSvgPathFromStroke(getStroke(segment))); + // this.pathData.set(allPaths.join(' ')); + // } + // } else { + // return; + // } + + const previousState = this.undoStack.pop(); + previousState ? this.redoStack.push(previousState): null; + + + if (previousState) { + this.undoStack[this.undoStack.length - 1]; + this.segments = previousState; + console.log(this.segments); + // Re-render all paths + const allPaths = this.segments.flatMap((segment) => this.getSvgPathFromStroke(getStroke(segment))); + this.pathData.set(allPaths.join(' ')); + // this.renderPath(); } - } else { - return; } } @@ -141,27 +160,21 @@ export class PicsaDrawingComponent { const currentState = this.redoStack.pop(); if (currentState) { this.undoStack.push(currentState); - this.points = currentState; - const stroke = getStroke(this.points); - const svgPath = this.getSvgPathFromStroke(stroke); - this.pathData.set(svgPath); + this.segments = currentState; + this.renderPath(); } } else { return; } } - public _startNewStroke() { + /* End the current stroke */ + public _endCurrentStroke() { if (this.points.length > 0) { - this._pushToUndo(); // Save the current stroke before starting a new one + this.segments.push([...this.points]); + this._pushToUndo(); } - this.points = []; // Clear the points for the new stroke - } - - // Method to end the current stroke - public _endCurrentStroke(event: PointerEvent) { - // this.addPointToPath(event.pageX, event.pageY); - // this.points = null as never - // this.points.push([0, 0, 0]) + this.points = []; + this.renderPath(); } } From 8561ea588668f4a7824506ab1b0da76903511f32 Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Tue, 28 May 2024 20:41:49 +0300 Subject: [PATCH 14/22] update html template --- .../features/drawing/drawing.component.html | 38 ++++++++++--------- .../features/drawing/drawing.component.scss | 6 ++- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/libs/shared/src/features/drawing/drawing.component.html b/libs/shared/src/features/drawing/drawing.component.html index 0b09b78e8..d24ae139d 100644 --- a/libs/shared/src/features/drawing/drawing.component.html +++ b/libs/shared/src/features/drawing/drawing.component.html @@ -1,24 +1,26 @@ -
- - -
-
- - @if(points){ - - } - +
+
+ + +
+
+ + @if(segments){ + + } + +
diff --git a/libs/shared/src/features/drawing/drawing.component.scss b/libs/shared/src/features/drawing/drawing.component.scss index ef13d4e85..523cc3e04 100644 --- a/libs/shared/src/features/drawing/drawing.component.scss +++ b/libs/shared/src/features/drawing/drawing.component.scss @@ -4,7 +4,7 @@ box-sizing: border-box; // mat-dialog has max-width 80vw and includes 24px padding so just fit max width: calc(80vw - 48px); - height: 80vh; + height: 70vh; } .flex.flex-row{ @@ -12,3 +12,7 @@ flex-direction: row; justify-content: flex-start; } + +.svg-container{ + height: 100%; +} From 1c748f05f4c1439c5bb495750c658e574ae00398 Mon Sep 17 00:00:00 2001 From: FaithDaka Date: Tue, 28 May 2024 21:23:06 +0300 Subject: [PATCH 15/22] refactor: undo function --- .../src/features/drawing/drawing.component.ts | 29 ++++--------------- 1 file changed, 6 insertions(+), 23 deletions(-) diff --git a/libs/shared/src/features/drawing/drawing.component.ts b/libs/shared/src/features/drawing/drawing.component.ts index 33024ca61..542f6e9cb 100644 --- a/libs/shared/src/features/drawing/drawing.component.ts +++ b/libs/shared/src/features/drawing/drawing.component.ts @@ -127,30 +127,13 @@ export class PicsaDrawingComponent { /* Undo previous Stroke render */ public undoSvgStroke() { if (this.undoStack.length > 0) { - // const currentState = this.undoStack.pop(); - // this.redoStack.push(currentState as Segment[]); - // if (currentState && currentState.length > 0) { - // this.segments = currentState; - // const allPaths = this.segments.map((segment) => this.getSvgPathFromStroke(getStroke(segment))); - // this.pathData.set(allPaths.join(' ')); - // } - // } else { - // return; - // } - const previousState = this.undoStack.pop(); - previousState ? this.redoStack.push(previousState): null; - - - if (previousState) { - this.undoStack[this.undoStack.length - 1]; - this.segments = previousState; - console.log(this.segments); - // Re-render all paths - const allPaths = this.segments.flatMap((segment) => this.getSvgPathFromStroke(getStroke(segment))); - this.pathData.set(allPaths.join(' ')); - // this.renderPath(); - } + previousState ? this.redoStack.push(previousState as Segment[]) : null; + + this.segments = this.undoStack[this.undoStack.length - 1]; + const allPaths = this.segments.flatMap((segment) => this.getSvgPathFromStroke(getStroke(segment))); + this.pathData.set(allPaths.join(' ')); + // this.renderPath(); } } From 1210c26eed7d385d9d0e0c45bfc677cfc2e2bf0f Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 4 Jun 2024 08:14:49 +0100 Subject: [PATCH 16/22] feat: optimise svg drawing --- .../features/drawing/drawing.component.html | 11 +- .../src/features/drawing/drawing.component.ts | 126 ++++++++++-------- 2 files changed, 78 insertions(+), 59 deletions(-) diff --git a/libs/shared/src/features/drawing/drawing.component.html b/libs/shared/src/features/drawing/drawing.component.html index d24ae139d..14b039f26 100644 --- a/libs/shared/src/features/drawing/drawing.component.html +++ b/libs/shared/src/features/drawing/drawing.component.html @@ -13,11 +13,18 @@ height="100%" (pointerdown)="handlePointerDown($event)" (pointermove)="handlePointerMove($event)" + (mouseup)="handlePointerUp()" (pointerup)="handlePointerUp()" style="touch-action: none" > - @if(segments){ - + + @for(segment of segments; track segment.id){ + + } + + + @if(activeSegment){ + }
diff --git a/libs/shared/src/features/drawing/drawing.component.ts b/libs/shared/src/features/drawing/drawing.component.ts index 542f6e9cb..ebc0bc8e8 100644 --- a/libs/shared/src/features/drawing/drawing.component.ts +++ b/libs/shared/src/features/drawing/drawing.component.ts @@ -1,20 +1,34 @@ import { CommonModule } from '@angular/common'; -import { Component, ElementRef, signal, ViewChild } from '@angular/core'; +import { ChangeDetectionStrategy, Component, ElementRef, signal, ViewChild, WritableSignal } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; -import { getStroke } from 'perfect-freehand'; +import { getStroke, StrokeOptions } from 'perfect-freehand'; + +import { generateID } from '../../services/core/db/db.service'; + +type Segment = { + /** Unique id for each segment for efficient tracking */ + id: string; + /** [x,y,pressure] point representation for current segment */ + points: [number, number, number][]; + /** Generate svg path */ + path: WritableSignal; +}; -type Point = [number, number, number]; -type Segment = Point[]; @Component({ selector: 'picsa-custom-drawing', standalone: true, imports: [CommonModule, MatDialogModule, MatButtonModule], templateUrl: './drawing.component.html', styleUrls: ['./drawing.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class PicsaDrawingComponent { - options = { + /** Number of pixel difference required to record new point */ + private tolerance = 5; + + /** Perfect-freehand configuration */ + strokeOptions: StrokeOptions = { size: 32, thinning: 0.5, smoothing: 0.5, @@ -32,14 +46,14 @@ export class PicsaDrawingComponent { }, }; - /** [x,y,pressure] point representation */ - public points: Segment = []; + /** List of segment currently being drawn */ + public activeSegment = this.createNewSegment(); + + /** List of all saved segments */ public segments: Segment[] = []; - public undoStack: Segment[][] = []; - public redoStack: Segment[][] = []; - /** SVG representation of active path segment */ - public pathData = signal(''); + /** List of segments removed pending redo */ + private redoStack: Segment[] = []; /** * When the svg is rendered in a parent element track position @@ -51,30 +65,50 @@ export class PicsaDrawingComponent { constructor(public dialog: MatDialog) {} + private createNewSegment() { + const segment: Segment = { id: generateID(5), path: signal(''), points: [] }; + return segment; + } + handlePointerDown(event: PointerEvent) { + // Set svg element as target for future pointer events (will release automatically on pointerup) + // https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture const target = event.target as SVGElement; target.setPointerCapture(event.pointerId); + + // Ensure all points are calculated relative to svg container this.calculateContainerOffset(); - this.addPointToPath(event.pageX, event.pageY); - this.renderPath(); + + // ensure previous segment finalised (in case pointerup not fired) and start new segment + if (this.activeSegment?.points.length > 0) { + this.finaliseActiveSegment(); + } + // ensure initial point set on active segment + this.addPointToActiveSegment(event.pageX, event.pageY); } handlePointerMove(event: PointerEvent) { if (event.buttons !== 1) return; - this.addPointToPath(event.pageX, event.pageY); - this.renderPath(); - this._pushToUndo(); + this.addPointToActiveSegment(event.pageX, event.pageY); } handlePointerUp() { - this._endCurrentStroke(); + this.finaliseActiveSegment(); } /** Add a point to the current path, adjusting absolute position for relative container */ - private addPointToPath(x: number, y: number, pressure = 0.5) { + private addPointToActiveSegment(x: number, y: number, pressure = 0.5) { + // calculate svg point position relative to container const [left, top] = this.containerOffset; - this.points.push([x - left, y - top, pressure]); - this.segments.push(this.points); + const currentX = x - left; + const currentY = y - top; + // check whether points differ significantly from previous and render accordingly + const lastPoint = this.activeSegment.points[this.activeSegment.points.length - 1]; + const [lastX, lastY] = lastPoint || [-1, -1]; + if (Math.abs(lastX - currentX) > this.tolerance || Math.abs(lastY - currentY) > this.tolerance) { + this.activeSegment.points.push([x - left, y - top, pressure]); + this.renderActiveSegment(); + } } /** Determine current positioning of svg drawing container to use for path offsets */ @@ -85,9 +119,10 @@ export class PicsaDrawingComponent { } /** Render an svg path element generated from current list of points */ - private renderPath() { - const allPaths = this.segments.flatMap((segment) => this.getSvgPathFromStroke(getStroke(segment))).join(' '); - this.pathData.set(allPaths); + private renderActiveSegment() { + const stroke = getStroke(this.activeSegment.points, this.strokeOptions); + const path = this.getSvgPathFromStroke(stroke); + this.activeSegment.path.set(path); } /** @@ -108,56 +143,33 @@ export class PicsaDrawingComponent { return d.join(' '); } - /* Push to Undo Stack */ - private _pushToUndo() { - if (this.points.length > 0) { - this.undoStack.push([...this.segments]); - this.redoStack = []; - } - } - /* Clear Draw */ public clearDraw() { + this.activeSegment = this.createNewSegment(); this.segments = []; - this.points = []; - this.renderPath(); - this._pushToUndo(); } /* Undo previous Stroke render */ public undoSvgStroke() { - if (this.undoStack.length > 0) { - const previousState = this.undoStack.pop(); - previousState ? this.redoStack.push(previousState as Segment[]) : null; - - this.segments = this.undoStack[this.undoStack.length - 1]; - const allPaths = this.segments.flatMap((segment) => this.getSvgPathFromStroke(getStroke(segment))); - this.pathData.set(allPaths.join(' ')); - // this.renderPath(); + const lastSegment = this.segments.pop(); + if (lastSegment) { + this.redoStack.push(lastSegment); } } /* Redo Stroke render */ public redoSvgStroke() { - if (this.redoStack.length) { - const currentState = this.redoStack.pop(); - if (currentState) { - this.undoStack.push(currentState); - this.segments = currentState; - this.renderPath(); - } - } else { - return; + const lastSegment = this.redoStack.pop(); + if (lastSegment) { + this.segments.push(lastSegment); } } /* End the current stroke */ - public _endCurrentStroke() { - if (this.points.length > 0) { - this.segments.push([...this.points]); - this._pushToUndo(); + public finaliseActiveSegment() { + if (this.activeSegment.points.length > 0) { + this.segments.push(this.activeSegment); + this.activeSegment = this.createNewSegment(); } - this.points = []; - this.renderPath(); } } From dfb7896b65f2dc66cd2822d4c030e44b9640f50f Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 4 Jun 2024 22:07:39 +0100 Subject: [PATCH 17/22] feat: add drawing precision and output --- .../src/features/drawing/drawing.component.ts | 81 +++++++++++++------ 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/libs/shared/src/features/drawing/drawing.component.ts b/libs/shared/src/features/drawing/drawing.component.ts index ebc0bc8e8..447139369 100644 --- a/libs/shared/src/features/drawing/drawing.component.ts +++ b/libs/shared/src/features/drawing/drawing.component.ts @@ -1,5 +1,13 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, ElementRef, signal, ViewChild, WritableSignal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + output, + signal, + ViewChild, + WritableSignal, +} from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { getStroke, StrokeOptions } from 'perfect-freehand'; @@ -27,6 +35,9 @@ export class PicsaDrawingComponent { /** Number of pixel difference required to record new point */ private tolerance = 5; + /** Number of decimal places svg path calculated to */ + private precision = 2; + /** Perfect-freehand configuration */ strokeOptions: StrokeOptions = { size: 32, @@ -63,11 +74,14 @@ export class PicsaDrawingComponent { @ViewChild('svgElement') svgElement: ElementRef; + onSave = output(); + constructor(public dialog: MatDialog) {} - private createNewSegment() { - const segment: Segment = { id: generateID(5), path: signal(''), points: [] }; - return segment; + public async save() { + // emit list of all generated path segments as final svg + const pathSegments = this.segments.map((s) => s.path()); + this.onSave.emit(pathSegments); } handlePointerDown(event: PointerEvent) { @@ -96,6 +110,11 @@ export class PicsaDrawingComponent { this.finaliseActiveSegment(); } + private createNewSegment() { + const segment: Segment = { id: generateID(5), path: signal(''), points: [] }; + return segment; + } + /** Add a point to the current path, adjusting absolute position for relative container */ private addPointToActiveSegment(x: number, y: number, pressure = 0.5) { // calculate svg point position relative to container @@ -121,28 +140,10 @@ export class PicsaDrawingComponent { /** Render an svg path element generated from current list of points */ private renderActiveSegment() { const stroke = getStroke(this.activeSegment.points, this.strokeOptions); - const path = this.getSvgPathFromStroke(stroke); + const path = getSvgPathFromStroke(stroke, this.precision); this.activeSegment.path.set(path); } - /** - * Generate an svg path from array of [x,y] point arrays - * Copied from https://github.com/steveruizok/perfect-freehand - * */ - private getSvgPathFromStroke(stroke: number[][]) { - if (!stroke.length) return ''; - const d = stroke.reduce( - (acc, [x0, y0], i, arr) => { - const [x1, y1] = arr[(i + 1) % arr.length]; - acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2); - return acc; - }, - ['M', ...stroke[0], 'Q'] - ); - d.push('Z'); - return d.join(' '); - } - /* Clear Draw */ public clearDraw() { this.activeSegment = this.createNewSegment(); @@ -173,3 +174,37 @@ export class PicsaDrawingComponent { } } } + +const average = (a: number, b: number) => (a + b) / 2; + +/** + * Generate an svg path from array of [x,y] point arrays + * Copied from https://github.com/steveruizok/perfect-freehand + * */ +function getSvgPathFromStroke(points: number[][], precision = 2, closed = true) { + const len = points.length; + + if (len < 2) { + return ``; + } + + let a = points[0]; + let b = points[1]; + const c = points[2]; + + let result = `M${a[0].toFixed(precision)},${a[1].toFixed(precision)} Q${b[0].toFixed(precision)},${b[1].toFixed( + precision + )} ${average(b[0], c[0]).toFixed(precision)},${average(b[1], c[1]).toFixed(precision)} T`; + + for (let i = 2, max = len - 1; i < max; i++) { + a = points[i]; + b = points[i + 1]; + result += `${average(a[0], b[0]).toFixed(precision)},${average(a[1], b[1]).toFixed(precision)} `; + } + + if (closed) { + result += 'Z'; + } + + return result; +} From 20fb5039cdec89beb5b654f77e5abd6adfcc198d Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 4 Jun 2024 22:44:53 +0100 Subject: [PATCH 18/22] chore: remove placeholder component --- .../budget-draw/budget-draw.component.html | 1 - .../budget-draw/budget-draw.component.scss | 0 .../budget-draw/budget-draw.component.spec.ts | 21 ------------------- .../budget-draw/budget-draw.component.ts | 11 ---------- .../app/components/budget-tool.components.ts | 3 +-- 5 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.html delete mode 100644 apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.scss delete mode 100644 apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.spec.ts delete mode 100644 apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.ts diff --git a/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.html b/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.html deleted file mode 100644 index c232790aa..000000000 --- a/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.scss b/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.spec.ts b/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.spec.ts deleted file mode 100644 index 82bbe8981..000000000 --- a/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { BudgetDrawComponent } from './budget-draw.component'; - -describe('BudgetDrawComponent', () => { - let component: BudgetDrawComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [BudgetDrawComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(BudgetDrawComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.ts b/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.ts deleted file mode 100644 index 0126f1bd6..000000000 --- a/apps/picsa-tools/budget-tool/src/app/components/budget-draw/budget-draw.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component } from '@angular/core'; -import { PicsaDrawingComponent } from '@picsa/shared/features'; - -@Component({ - selector: 'budget-draw', - standalone:true, - imports: [PicsaDrawingComponent], - templateUrl: './budget-draw.component.html', - styleUrl: './budget-draw.component.scss', -}) -export class BudgetDrawComponent {} diff --git a/apps/picsa-tools/budget-tool/src/app/components/budget-tool.components.ts b/apps/picsa-tools/budget-tool/src/app/components/budget-tool.components.ts index 5ea51430e..8720e7590 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/budget-tool.components.ts +++ b/apps/picsa-tools/budget-tool/src/app/components/budget-tool.components.ts @@ -14,7 +14,6 @@ import { BudgetMaterialModule } from '../material.module'; // Components import { BudgetBalanceDotValueComponent } from './balance/balance-dot-value/dot-value'; import { BudgetBalanceLegendComponent } from './balance/balance-legend/balance-legend'; -import { BudgetDrawComponent } from './budget-draw/budget-draw.component'; import { BudgetSummaryComponent } from './budget-summary/budget-summary.component'; import { BudgetCardComponent } from './card/budget-card'; import { BudgetCardImageComponent } from './card/card-image/budget-card-image'; @@ -72,7 +71,7 @@ const components = [ RouterModule, MatTooltipModule, MatIconModule, - PicsaDrawingComponent + PicsaDrawingComponent, ], exports: components, }) From dc178d5a5793ff767db1eb2c7902ccc3004c9ab5 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 4 Jun 2024 22:47:13 +0100 Subject: [PATCH 19/22] chore: code tidying --- .../card/card-new/card-new-dialog.scss | 4 ++ .../card/card-new/card-new-dialog.ts | 1 + .../app/components/card/card-new/card-new.ts | 1 - .../src/app/pages/home/budget-home.module.ts | 2 - .../src/app/pages/home/budget-home.page.html | 7 +-- .../features/drawing/drawing.component.html | 62 ++++++++++--------- .../features/drawing/drawing.component.scss | 32 +++++++--- .../src/features/drawing/drawing.component.ts | 25 ++++---- 8 files changed, 76 insertions(+), 58 deletions(-) diff --git a/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.scss b/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.scss index c79cc9662..40522116a 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.scss +++ b/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.scss @@ -11,3 +11,7 @@ background: #dcdcdc; overflow: auto; } +.label-input { + width: 100%; + margin-top: 1rem; +} diff --git a/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.ts b/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.ts index 38b25b280..a416a8357 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.ts +++ b/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.ts @@ -9,6 +9,7 @@ import { IBudgetCard } from '../../../schema'; templateUrl: './card-new-dialog.html', styleUrls: ['./card-new-dialog.scss'], }) +// eslint-disable-next-line @angular-eslint/component-class-suffix export class BudgetCardNewDialog { public card: IBudgetCard; constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) card: IBudgetCard) { diff --git a/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new.ts b/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new.ts index b53bc8521..812d092c6 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new.ts +++ b/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new.ts @@ -33,7 +33,6 @@ export class BudgetCardNew { imgType: 'svg', }; const dialogRef = this.dialog.open(BudgetCardNewDialog, { - width: '250px', data: card, }); diff --git a/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.module.ts b/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.module.ts index f90030eb0..a772307cd 100644 --- a/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.module.ts +++ b/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.module.ts @@ -9,7 +9,6 @@ import { MobxAngularModule } from 'mobx-angular'; import { BudgetToolComponentsModule } from '../../components/budget-tool.components'; import { BudgetMaterialModule } from '../../material.module'; import { BudgetHomePage } from './budget-home.page'; -import { BudgetDrawComponent } from '../../components/budget-draw/budget-draw.component'; const routes: Routes = [ { @@ -28,7 +27,6 @@ const routes: Routes = [ BudgetMaterialModule, BudgetToolComponentsModule, MobxAngularModule, - BudgetDrawComponent ], declarations: [BudgetHomePage], }) diff --git a/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.page.html b/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.page.html index 53a431a97..0d8a17753 100644 --- a/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.page.html +++ b/apps/picsa-tools/budget-tool/src/app/pages/home/budget-home.page.html @@ -9,12 +9,7 @@ - -
-

Draw here!

- -
- +

{{ 'Saved Budgets' | translate }}

Click to Draw - - +
+
+ + + +
-
- - -
-
- - - @for(segment of segments; track segment.id){ - - } + + + @for(segment of segments; track segment.id){ + + } - - @if(activeSegment){ - - } - + + @if(activeSegment){ + + } + + + @if(!activeSegment){ + - + + }
- +
diff --git a/libs/shared/src/features/drawing/drawing.component.scss b/libs/shared/src/features/drawing/drawing.component.scss index 523cc3e04..a96c3bbaa 100644 --- a/libs/shared/src/features/drawing/drawing.component.scss +++ b/libs/shared/src/features/drawing/drawing.component.scss @@ -1,18 +1,32 @@ -.svg-dialog-container { +.dialog-container { + text-align: center; +} +.svg-container { border: 1px solid #eeeeee; border-radius: 4px; box-sizing: border-box; // mat-dialog has max-width 80vw and includes 24px padding so just fit max width: calc(80vw - 48px); - height: 70vh; + height: calc(80vw - 48px); + max-width: 66vh; + max-height: 66vh; + position: relative; } - -.flex.flex-row{ - display: flex; - flex-direction: row; - justify-content: flex-start; +.draw-logo { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--color-light); + mat-icon { + font-size: 48px; + height: 48px; + width: 48px; + } } -.svg-container{ - height: 100%; +.edit-buttons { + display: flex; + gap: 8px; + margin-bottom: 8px; } diff --git a/libs/shared/src/features/drawing/drawing.component.ts b/libs/shared/src/features/drawing/drawing.component.ts index 447139369..5b2ef3841 100644 --- a/libs/shared/src/features/drawing/drawing.component.ts +++ b/libs/shared/src/features/drawing/drawing.component.ts @@ -10,8 +10,10 @@ import { } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; import { getStroke, StrokeOptions } from 'perfect-freehand'; +import { PicsaTranslateModule } from '../../modules'; import { generateID } from '../../services/core/db/db.service'; type Segment = { @@ -26,7 +28,7 @@ type Segment = { @Component({ selector: 'picsa-custom-drawing', standalone: true, - imports: [CommonModule, MatDialogModule, MatButtonModule], + imports: [CommonModule, MatButtonModule, MatDialogModule, MatIconModule, PicsaTranslateModule], templateUrl: './drawing.component.html', styleUrls: ['./drawing.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, @@ -40,7 +42,7 @@ export class PicsaDrawingComponent { /** Perfect-freehand configuration */ strokeOptions: StrokeOptions = { - size: 32, + size: 24, thinning: 0.5, smoothing: 0.5, streamline: 0.5, @@ -58,7 +60,7 @@ export class PicsaDrawingComponent { }; /** List of segment currently being drawn */ - public activeSegment = this.createNewSegment(); + public activeSegment: Segment; /** List of all saved segments */ public segments: Segment[] = []; @@ -74,16 +76,10 @@ export class PicsaDrawingComponent { @ViewChild('svgElement') svgElement: ElementRef; - onSave = output(); + onChange = output(); constructor(public dialog: MatDialog) {} - public async save() { - // emit list of all generated path segments as final svg - const pathSegments = this.segments.map((s) => s.path()); - this.onSave.emit(pathSegments); - } - handlePointerDown(event: PointerEvent) { // Set svg element as target for future pointer events (will release automatically on pointerup) // https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture @@ -93,6 +89,10 @@ export class PicsaDrawingComponent { // Ensure all points are calculated relative to svg container this.calculateContainerOffset(); + if (!this.activeSegment) { + this.activeSegment = this.createNewSegment(); + } + // ensure previous segment finalised (in case pointerup not fired) and start new segment if (this.activeSegment?.points.length > 0) { this.finaliseActiveSegment(); @@ -108,6 +108,9 @@ export class PicsaDrawingComponent { handlePointerUp() { this.finaliseActiveSegment(); + // output current full svg + const pathSegments = this.segments.map((s) => s.path()); + this.onChange.emit(pathSegments); } private createNewSegment() { @@ -146,7 +149,7 @@ export class PicsaDrawingComponent { /* Clear Draw */ public clearDraw() { - this.activeSegment = this.createNewSegment(); + this.activeSegment = undefined as any; this.segments = []; } From 873accbc07f8bba4ad62ad13c5ac6203af8c6bb8 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 4 Jun 2024 22:56:15 +0100 Subject: [PATCH 20/22] chore: code tidying --- .../src/features/drawing/drawing.component.html | 4 ++-- .../src/features/drawing/drawing.component.ts | 17 +++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/libs/shared/src/features/drawing/drawing.component.html b/libs/shared/src/features/drawing/drawing.component.html index d23709bc1..e06df8838 100644 --- a/libs/shared/src/features/drawing/drawing.component.html +++ b/libs/shared/src/features/drawing/drawing.component.html @@ -21,12 +21,12 @@ } - @if(activeSegment){ + @if(activeSegment.path()){ } - @if(!activeSegment){ + @if(segments.length===0 && !activeSegment.path()){ -
+
@for(segment of segments; track segment.id){ - + } @if(activeSegment.path()){ - + } diff --git a/libs/shared/src/features/drawing/drawing.component.scss b/libs/shared/src/features/drawing/drawing.component.scss index a96c3bbaa..76a6b4933 100644 --- a/libs/shared/src/features/drawing/drawing.component.scss +++ b/libs/shared/src/features/drawing/drawing.component.scss @@ -5,12 +5,8 @@ border: 1px solid #eeeeee; border-radius: 4px; box-sizing: border-box; - // mat-dialog has max-width 80vw and includes 24px padding so just fit max - width: calc(80vw - 48px); - height: calc(80vw - 48px); - max-width: 66vh; - max-height: 66vh; position: relative; + overflow: hidden; } .draw-logo { position: absolute; diff --git a/libs/shared/src/features/drawing/drawing.component.ts b/libs/shared/src/features/drawing/drawing.component.ts index 46cd83c65..d4380f6db 100644 --- a/libs/shared/src/features/drawing/drawing.component.ts +++ b/libs/shared/src/features/drawing/drawing.component.ts @@ -2,7 +2,9 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, + computed, ElementRef, + input, output, signal, ViewChild, @@ -34,6 +36,28 @@ type Segment = { changeDetection: ChangeDetectionStrategy.OnPush, }) export class PicsaDrawingComponent { + /** SVG drawing viewbox and container size */ + size = input(384); + + viewbox = computed(() => `0 0 ${this.size()} ${this.size()}`); + + public onChange = output(); + + /** List of segment currently being drawn */ + public activeSegment = this.createNewSegment(); + + /** List of all saved segments */ + public segments: Segment[] = []; + + /** List of segments removed pending redo */ + private redoStack: Segment[] = []; + + /** + * When the svg is rendered in a parent element track position + * to use to offset + */ + private containerOffset = [0, 0]; + /** Number of pixel difference required to record new point */ private tolerance = 5; @@ -41,7 +65,7 @@ export class PicsaDrawingComponent { private precision = 2; /** Perfect-freehand configuration */ - strokeOptions: StrokeOptions = { + private strokeOptions: StrokeOptions = { size: 24, thinning: 0.5, smoothing: 0.5, @@ -59,25 +83,8 @@ export class PicsaDrawingComponent { }, }; - /** List of segment currently being drawn */ - public activeSegment = this.createNewSegment(); - - /** List of all saved segments */ - public segments: Segment[] = []; - - /** List of segments removed pending redo */ - private redoStack: Segment[] = []; - - /** - * When the svg is rendered in a parent element track position - * to use to offset - */ - private containerOffset = [0, 0]; - @ViewChild('svgElement') svgElement: ElementRef; - onChange = output(); - constructor(public dialog: MatDialog) {} handlePointerDown(event: PointerEvent) { @@ -106,8 +113,18 @@ export class PicsaDrawingComponent { this.finaliseActiveSegment(); } // output current full svg - const pathSegments = this.segments.map((s) => s.path()); - this.onChange.emit(pathSegments); + const data = this.sanitizeOutputSVG(); + this.onChange.emit(data); + } + + /** Replace ngcontent, inserted classes and comments from svg */ + private sanitizeOutputSVG() { + const svgEl = this.svgElement.nativeElement.outerHTML; + return svgEl + .replace(/_ngcontent(\S)* /gi, '') + .replace(/class="[^"]*"/gi, '') + .replace(//gi, '') + .replace('style="touch-action: none;"', ''); } private createNewSegment() { From f5ab6a66d21f3e73674c5f1eda825fd5950657f9 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 5 Jun 2024 10:29:26 +0100 Subject: [PATCH 22/22] feat: integrate drawing component with budget tool --- .../card/card-image/budget-card-image.ts | 10 ++++------ .../components/card/card-new/card-new-dialog.html | 11 +++++++---- .../components/card/card-new/card-new-dialog.ts | 14 +++++++++++--- .../src/features/drawing/drawing.component.html | 1 + 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/apps/picsa-tools/budget-tool/src/app/components/card/card-image/budget-card-image.ts b/apps/picsa-tools/budget-tool/src/app/components/card/card-image/budget-card-image.ts index 911a08742..d8deafd12 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/card/card-image/budget-card-image.ts +++ b/apps/picsa-tools/budget-tool/src/app/components/card/card-image/budget-card-image.ts @@ -61,17 +61,15 @@ export class BudgetCardImageComponent implements OnInit, OnDestroy { // so convert to html that embeds within an tag and div innerhtml private convertSVGToImageData(svgTag: string) { const encodedSVG = this._encodeSVG(svgTag); - const Html = ``; + const Html = ``; return this.sanitizer.bypassSecurityTrustHtml(Html); } // method taken from http://yoksel.github.io/url-encoder/ // applies selective replacement of uri characters private _encodeSVG(data: string): string { - const symbols = /[\r\n%#()<>?[\\\]^`{|}]/g; - data = data.replace(/"/g, "'"); - data = data.replace(/>\s{1,}<'); - data = data.replace(/\s{2,}/g, ' '); - return data.replace(symbols, encodeURIComponent); + // ignore already encoded + if (data.startsWith('data:')) return data; + return `data:image/svg+xml;base64,${btoa(data)}`; } } diff --git a/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.html b/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.html index bbf23d05b..9cb9ef017 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.html +++ b/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.html @@ -1,10 +1,13 @@ - + + + + + {{'Card Label' | translate}} - diff --git a/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.ts b/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.ts index a416a8357..09a587430 100644 --- a/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.ts +++ b/apps/picsa-tools/budget-tool/src/app/components/card/card-new/card-new-dialog.ts @@ -15,10 +15,18 @@ export class BudgetCardNewDialog { constructor(public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) card: IBudgetCard) { this.card = card; } + + public imgData: string; + + public setBudgetDrawing(svgData: string) { + if (svgData) { + this.imgData = `data:image/svg+xml;base64,${btoa(svgData)}`; + } + } save() { - this.card.id = this.card.label.replace(/\s+/g, '-').toLowerCase(); + this.card.id = `custom_${this.card.label.replace(/\s+/g, '-').toLowerCase()}`; this.card.customMeta = { - imgData: this.generateImage(this.card.label), + imgData: this.imgData, dateCreated: new Date().toISOString(), createdBy: '', }; @@ -27,7 +35,7 @@ export class BudgetCardNewDialog { // return an svg circle with text in the middle // text is either first 2 initials (if multiple words) or first 2 letters (if one word) - generateImage(text: string) { + private generateImage(text: string) { const byWord = text.split(' '); const abbr = byWord.length > 1 ? `${byWord[0].charAt(0)}.${byWord[1].charAt(0)}` : text.substring(0, 2); return `