From a171b92bf83c432cf38396bd58afdfc3fc8b9fe8 Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Thu, 17 Oct 2024 03:09:35 +0300 Subject: [PATCH 01/14] Tool panel resize buttons work with bugs --- firebird-ng/src/app/app.component.html | 49 ------------- .../tool-panel/tool-panel.component.html | 27 +++++++ .../tool-panel/tool-panel.component.scss | 50 +++++++++++++ .../tool-panel/tool-panel.component.spec.ts | 23 ++++++ .../tool-panel/tool-panel.component.ts | 72 +++++++++++++++++++ .../main-display/main-display.component.html | 7 +- .../main-display/main-display.component.ts | 3 +- 7 files changed, 178 insertions(+), 53 deletions(-) create mode 100644 firebird-ng/src/app/components/tool-panel/tool-panel.component.html create mode 100644 firebird-ng/src/app/components/tool-panel/tool-panel.component.scss create mode 100644 firebird-ng/src/app/components/tool-panel/tool-panel.component.spec.ts create mode 100644 firebird-ng/src/app/components/tool-panel/tool-panel.component.ts diff --git a/firebird-ng/src/app/app.component.html b/firebird-ng/src/app/app.component.html index 7e4a848..f54f8ac 100644 --- a/firebird-ng/src/app/app.component.html +++ b/firebird-ng/src/app/app.component.html @@ -51,55 +51,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/firebird-ng/src/app/components/tool-panel/tool-panel.component.html b/firebird-ng/src/app/components/tool-panel/tool-panel.component.html new file mode 100644 index 0000000..0f84d33 --- /dev/null +++ b/firebird-ng/src/app/components/tool-panel/tool-panel.component.html @@ -0,0 +1,27 @@ +
+
+
+ +
+ + +
+
+
+
diff --git a/firebird-ng/src/app/components/tool-panel/tool-panel.component.scss b/firebird-ng/src/app/components/tool-panel/tool-panel.component.scss new file mode 100644 index 0000000..5b49f22 --- /dev/null +++ b/firebird-ng/src/app/components/tool-panel/tool-panel.component.scss @@ -0,0 +1,50 @@ +.tool-panel-wrapper { + position: fixed; + top: 20%; + right: 0; + display: flex; + align-items: center; +} + +.tool-panel { + width: 60px; + height: auto; + background-color: #2e2e2e; + box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1); + border-radius: 8px 0 0 8px; + display: flex; + flex-direction: column; + align-items: center; + transition: transform 0.3s ease; + padding: 10px; +} + +.tool-panel.collapsed { + transform: translateX(calc(100% - 60px)); +} + +.icon-button { + background: none; + border: none; + cursor: pointer; + padding: 10px; + display: flex; + justify-content: center; + align-items: center; + + border-radius: 50%; + transition: background-color 0.3s ease; + + &:hover { + background-color: #5c5c5c; + } + + mat-icon { + font-size: 24px; + color: white; + } +} + +.toggle-button { + z-index: 1; +} diff --git a/firebird-ng/src/app/components/tool-panel/tool-panel.component.spec.ts b/firebird-ng/src/app/components/tool-panel/tool-panel.component.spec.ts new file mode 100644 index 0000000..f8b35ca --- /dev/null +++ b/firebird-ng/src/app/components/tool-panel/tool-panel.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ToolPanelComponent } from './tool-panel.component'; + +describe('ToolPanelComponent', () => { + let component: ToolPanelComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ToolPanelComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ToolPanelComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts b/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts new file mode 100644 index 0000000..5cf4f8e --- /dev/null +++ b/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts @@ -0,0 +1,72 @@ +import {Component, Input} from '@angular/core'; +import {NgIf} from "@angular/common"; +import {MatIcon} from "@angular/material/icon"; +import { EventDisplayService } from 'phoenix-ui-components'; + +@Component({ + selector: 'app-tool-panel', + standalone: true, + imports: [ + NgIf, + MatIcon + ], + templateUrl: './tool-panel.component.html', + styleUrl: './tool-panel.component.scss' +}) +export class ToolPanelComponent { + isCollapsed = false; + + /** Factor to zoom by. */ + private zoomFactor: number = 1.1; + /** Timeout for clearing mouse hold. */ + private zoomTimeout: any; + /** The speed and time of zoom. */ + private zoomTime: number = 100; + + constructor(private eventDisplay: EventDisplayService) {} + + /** + * Zoom all the cameras by a specific zoom factor. + * The factor may either be greater (zoom in) or smaller (zoom out) than 1. + * @param zoomFactor The factor to zoom by. + */ + zoomTo(zoomFactor: number) { + this.zoomTime = + this.zoomTime > 30 ? Math.floor(this.zoomTime / 1.1) : this.zoomTime; + + this.eventDisplay.zoomTo(zoomFactor, this.zoomTime); + + this.zoomTimeout = setTimeout(() => { + this.zoomTo(zoomFactor); + }, this.zoomTime); + } + + onLeftClick(event: MouseEvent, action: string) { + if (event.button === 0) { + if (action === 'zoomIn') { + this.zoomIn(); + } else if (action === 'zoomOut') { + this.zoomOut(); + } + } + } + + zoomIn() { + this.zoomTo(1 / this.zoomFactor); + } + zoomOut() { + this.zoomTo(this.zoomFactor); + } + + clearZoom() { + this.zoomTime = 100; + clearTimeout(this.zoomTimeout); + } + + togglePanel() { + this.isCollapsed = !this.isCollapsed; + } + + + +} diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.html b/firebird-ng/src/app/pages/main-display/main-display.component.html index 2ae68f7..8d5144f 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.html +++ b/firebird-ng/src/app/pages/main-display/main-display.component.html @@ -166,8 +166,8 @@ - - + + @@ -227,7 +227,7 @@
- +
@@ -279,4 +279,5 @@
+ diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.ts b/firebird-ng/src/app/pages/main-display/main-display.component.ts index c404794..468f183 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.ts +++ b/firebird-ng/src/app/pages/main-display/main-display.component.ts @@ -47,6 +47,7 @@ import {SceneTreeComponent} from "../geometry-tree/scene-tree.component"; import {DisplayShellComponent} from "../../components/display-shell/display-shell.component"; import {DataModelPainter} from "../../painters/data-model-painter"; import {AppComponent} from "../../app.component"; +import {ToolPanelComponent} from "../../components/tool-panel/tool-panel.component"; // import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; @@ -55,7 +56,7 @@ import {AppComponent} from "../../app.component"; @Component({ selector: 'app-test-experiment', templateUrl: './main-display.component.html', - imports: [PhoenixUIModule, IoOptionsComponent, MatSlider, MatIcon, MatButton, MatSliderThumb, DecimalPipe, MatTooltip, MatFormField, MatSelect, MatOption, NgForOf, AngularSplitModule, SceneTreeComponent, NgClass, MatIconButton, DisplayShellComponent, AppComponent, RouterOutlet, RouterLink], + imports: [PhoenixUIModule, IoOptionsComponent, MatSlider, MatIcon, MatButton, MatSliderThumb, DecimalPipe, MatTooltip, MatFormField, MatSelect, MatOption, NgForOf, AngularSplitModule, SceneTreeComponent, NgClass, MatIconButton, DisplayShellComponent, AppComponent, RouterOutlet, RouterLink, ToolPanelComponent], standalone: true, styleUrls: ['./main-display.component.scss'] }) From d61c75c9afb5c549db4bdf9ca294042b59d548ff Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Thu, 17 Oct 2024 23:00:27 +0300 Subject: [PATCH 02/14] Updating toolpanel zoom is working, new buttons added --- .../tool-panel/tool-panel.component.html | 2 ++ .../tool-panel/tool-panel.component.scss | 4 ++- .../tool-panel/tool-panel.component.ts | 29 ++++++++++--------- .../main-display/main-display.component.html | 26 ++++++++--------- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/firebird-ng/src/app/components/tool-panel/tool-panel.component.html b/firebird-ng/src/app/components/tool-panel/tool-panel.component.html index 0f84d33..05d390b 100644 --- a/firebird-ng/src/app/components/tool-panel/tool-panel.component.html +++ b/firebird-ng/src/app/components/tool-panel/tool-panel.component.html @@ -21,6 +21,8 @@ (touchcancel)="clearZoom()"> zoom_out + + diff --git a/firebird-ng/src/app/components/tool-panel/tool-panel.component.scss b/firebird-ng/src/app/components/tool-panel/tool-panel.component.scss index 5b49f22..0e87eee 100644 --- a/firebird-ng/src/app/components/tool-panel/tool-panel.component.scss +++ b/firebird-ng/src/app/components/tool-panel/tool-panel.component.scss @@ -3,6 +3,7 @@ top: 20%; right: 0; display: flex; + justify-content: center; align-items: center; } @@ -15,6 +16,7 @@ display: flex; flex-direction: column; align-items: center; + justify-content: center; transition: transform 0.3s ease; padding: 10px; } @@ -31,7 +33,7 @@ display: flex; justify-content: center; align-items: center; - + margin-left: 7px; border-radius: 50%; transition: background-color 0.3s ease; diff --git a/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts b/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts index 5cf4f8e..0f4e2bc 100644 --- a/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts +++ b/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts @@ -1,14 +1,16 @@ import {Component, Input} from '@angular/core'; import {NgIf} from "@angular/common"; import {MatIcon} from "@angular/material/icon"; -import { EventDisplayService } from 'phoenix-ui-components'; +import {EventDisplayService, PhoenixUIModule} from 'phoenix-ui-components'; +import {PhoenixThreeFacade} from "../../utils/phoenix-three-facade"; @Component({ selector: 'app-tool-panel', standalone: true, imports: [ NgIf, - MatIcon + MatIcon, + PhoenixUIModule ], templateUrl: './tool-panel.component.html', styleUrl: './tool-panel.component.scss' @@ -16,6 +18,7 @@ import { EventDisplayService } from 'phoenix-ui-components'; export class ToolPanelComponent { isCollapsed = false; + private threeFacade: PhoenixThreeFacade; /** Factor to zoom by. */ private zoomFactor: number = 1.1; /** Timeout for clearing mouse hold. */ @@ -23,22 +26,22 @@ export class ToolPanelComponent { /** The speed and time of zoom. */ private zoomTime: number = 100; - constructor(private eventDisplay: EventDisplayService) {} + constructor( + private eventDisplay: EventDisplayService) + { + this.threeFacade = new PhoenixThreeFacade(this.eventDisplay); + } /** * Zoom all the cameras by a specific zoom factor. * The factor may either be greater (zoom in) or smaller (zoom out) than 1. - * @param zoomFactor The factor to zoom by. + * @param factor */ - zoomTo(zoomFactor: number) { - this.zoomTime = - this.zoomTime > 30 ? Math.floor(this.zoomTime / 1.1) : this.zoomTime; - - this.eventDisplay.zoomTo(zoomFactor, this.zoomTime); - - this.zoomTimeout = setTimeout(() => { - this.zoomTo(zoomFactor); - }, this.zoomTime); + zoomTo(factor: number) { + let orbitControls = this.threeFacade.activeOrbitControls; + let camera = this.threeFacade.mainCamera; + orbitControls.object.position.subVectors(camera.position, orbitControls.target).multiplyScalar(factor).add(orbitControls.target); + orbitControls.update(); } onLeftClick(event: MouseEvent, action: string) { diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.html b/firebird-ng/src/app/pages/main-display/main-display.component.html index 8d5144f..6445f2e 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.html +++ b/firebird-ng/src/app/pages/main-display/main-display.component.html @@ -169,7 +169,7 @@ - + @@ -179,26 +179,26 @@ - - + + - - + + - - + + - - + + - - + + - - + + From 7f783906279c87d92ff96e3b8193702476de7b8f Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Thu, 17 Oct 2024 16:59:47 -0400 Subject: [PATCH 03/14] Update libraries and fix component painter --- firebird-ng/package-lock.json | 198 +++++------------- firebird-ng/package.json | 48 ++--- .../display-shell.component.scss | 6 +- .../main-display/main-display.component.ts | 3 +- .../src/app/painters/data-model-painter.ts | 9 +- .../src/app/services/data-model.service.ts | 4 +- .../test_data/hit-box.v0.01.firebird.json.zip | Bin 40432 -> 44252 bytes 7 files changed, 85 insertions(+), 183 deletions(-) diff --git a/firebird-ng/package-lock.json b/firebird-ng/package-lock.json index 2f824fc..26797a0 100644 --- a/firebird-ng/package-lock.json +++ b/firebird-ng/package-lock.json @@ -8,21 +8,21 @@ "name": "firebird", "version": "0.0.5", "dependencies": { - "@angular/animations": "^17.3.0", - "@angular/cdk": "~17.3.7", - "@angular/common": "^17.3.0", - "@angular/compiler": "^17.3.0", - "@angular/core": "^17.3.0", - "@angular/forms": "^17.3.0", - "@angular/material": "~17.3.7", - "@angular/platform-browser": "^17.3.0", - "@angular/platform-browser-dynamic": "^17.3.0", - "@angular/router": "^17.3.0", - "@tweenjs/tween.js": "^23.1.2", - "@types/picomatch": "^2.3.3", - "angular-split": "^17.1.1", + "@angular/animations": "^17.3.12", + "@angular/cdk": "~17.3.10", + "@angular/common": "^17.3.12", + "@angular/compiler": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/forms": "^17.3.12", + "@angular/material": "~17.3.10", + "@angular/platform-browser": "^17.3.12", + "@angular/platform-browser-dynamic": "^17.3.12", + "@angular/router": "^17.3.12", + "@tweenjs/tween.js": "^23.1.3", + "@types/picomatch": "^2.3.4", + "angular-split": "^17.2.0", "component": "^1.1.0", - "jsdom": "^24.0.0", + "jsdom": "^24.1.3", "jsonc-parser": "^3.3.1", "jsrootdi": "^7.6.101", "jszip": "^3.10.1", @@ -31,26 +31,26 @@ "phoenix-event-display": "^2.16.0", "phoenix-ui-components": "^2.16.0", "picomatch": "^4.0.2", - "rxjs": "~7.8.0", + "rxjs": "~7.8.1", "three": "^0.164.1", - "tslib": "^2.3.0", + "tslib": "^2.7.0", "vm": "^0.1.0", - "zone.js": "~0.14.3" + "zone.js": "~0.14.10" }, "devDependencies": { "@angular-builders/custom-webpack": "^17.0.2", "@angular-devkit/build-angular": "^17.3.10", - "@angular/cli": "^17.3.1", - "@angular/compiler-cli": "^17.3.0", - "@types/jasmine": "~5.1.0", + "@angular/cli": "^17.3.10", + "@angular/compiler-cli": "^17.3.12", + "@types/jasmine": "~5.1.4", "@types/three": "^0.164.0", - "jasmine-core": "~5.1.0", - "karma": "~6.4.0", + "jasmine-core": "~5.4.0", + "karma": "~6.4.4", "karma-chrome-launcher": "~3.2.0", - "karma-coverage": "~2.2.0", + "karma-coverage": "~2.2.1", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", - "typescript": "~5.4.2", + "typescript": "~5.4.5", "webpack-bundle-analyzer": "^4.10.2" } }, @@ -104,13 +104,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1703.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.9.tgz", - "integrity": "sha512-kEPfTOVnzrJxPGTvaXy8653HU9Fucxttx9gVfQR1yafs+yIEGx3fKGKe89YPmaEay32bIm7ZUpxDF1FO14nkdQ==", + "version": "0.1703.10", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.10.tgz", + "integrity": "sha512-wmjx5GspSPprdUGryK5+9vNawbEO7p8h9dxgX3uoeFwPAECcHC+/KK3qPhX2NiGcM6MDsyt25SrbSktJp6PRsA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "17.3.9", + "@angular-devkit/core": "17.3.10", "rxjs": "7.8.1" }, "engines": { @@ -248,48 +248,6 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/architect": { - "version": "0.1703.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.10.tgz", - "integrity": "sha512-wmjx5GspSPprdUGryK5+9vNawbEO7p8h9dxgX3uoeFwPAECcHC+/KK3qPhX2NiGcM6MDsyt25SrbSktJp6PRsA==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "17.3.10", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { - "version": "17.3.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.10.tgz", - "integrity": "sha512-czdl54yxU5DOAGy/uUPNjJruoBDTgwi/V+eOgLNybYhgrc+TsY0f7uJ11yEk/pz5sCov7xIiS7RdRv96waS7vg==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, "node_modules/@angular-devkit/build-angular/node_modules/jsonc-parser": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", @@ -336,71 +294,11 @@ "webpack-dev-server": "^4.0.0" } }, - "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/architect": { - "version": "0.1703.10", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.10.tgz", - "integrity": "sha512-wmjx5GspSPprdUGryK5+9vNawbEO7p8h9dxgX3uoeFwPAECcHC+/KK3qPhX2NiGcM6MDsyt25SrbSktJp6PRsA==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "17.3.10", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/build-webpack/node_modules/@angular-devkit/core": { + "node_modules/@angular-devkit/core": { "version": "17.3.10", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.10.tgz", "integrity": "sha512-czdl54yxU5DOAGy/uUPNjJruoBDTgwi/V+eOgLNybYhgrc+TsY0f7uJ11yEk/pz5sCov7xIiS7RdRv96waS7vg==", "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.1", - "picomatch": "4.0.1", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.13.0 || >=20.9.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/build-webpack/node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true - }, - "node_modules/@angular-devkit/build-webpack/node_modules/picomatch": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", - "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@angular-devkit/core": { - "version": "17.3.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.9.tgz", - "integrity": "sha512-/iKyn5YT7NW5ylrg9yufUydS8byExeQ2HHIwFC4Ebwb/JYYCz+k4tBf2LdP+zXpemDpLznXTQGWia0/yJjG8Vg==", - "dev": true, "license": "MIT", "dependencies": { "ajv": "8.12.0", @@ -445,13 +343,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "17.3.9", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.9.tgz", - "integrity": "sha512-9qg+uWywgAtaQlvbnCQv47hcL6ZuA+d9ucgZ0upZftBllZ2vp5WIthCPb2mB0uBkj84Csmtz9MsErFjOQtTj4g==", + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.10.tgz", + "integrity": "sha512-FHcNa1ktYRd0SKExCsNJpR75RffsyuPIV8kvBXzXnLHmXMqvl25G2te3yYJ9yYqy9OLy/58HZznZTxWRyUdHOg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "17.3.9", + "@angular-devkit/core": "17.3.10", "jsonc-parser": "3.2.1", "magic-string": "0.30.8", "ora": "5.4.1", @@ -503,16 +401,16 @@ } }, "node_modules/@angular/cli": { - "version": "17.3.9", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.9.tgz", - "integrity": "sha512-b5RGu5RO4VKZlMQDatwABAn1qocgD9u4IrGN2dvHDcrz5apTKYftUdGyG42vngyDNBCg1mWkSDQEWK4f2HfuGg==", + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.10.tgz", + "integrity": "sha512-lA0kf4Cpo8Jcuennq6wGyBTP/UG1oX4xsM9uLRZ2vkPoisjHCk46rWaVP7vfAqdUH39vbATFXftpy1SiEmAI4w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1703.9", - "@angular-devkit/core": "17.3.9", - "@angular-devkit/schematics": "17.3.9", - "@schematics/angular": "17.3.9", + "@angular-devkit/architect": "0.1703.10", + "@angular-devkit/core": "17.3.10", + "@angular-devkit/schematics": "17.3.10", + "@schematics/angular": "17.3.10", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", "ini": "4.1.2", @@ -4788,14 +4686,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "17.3.9", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.9.tgz", - "integrity": "sha512-q6N8mbcYC6cgPyjTrMH7ehULQoUUwEYN4g7uo4ylZ/PFklSLJvpSp4BuuxANgW449qHSBvQfdIoui9ayAUXQzA==", + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.10.tgz", + "integrity": "sha512-cI+VB/WXlOeAMamni932lE/AZgui8o81dMyEXNXqCuYagNAMuKXliW79Mi5BwYQEABv/BUb4hB4zYtbQqHyACA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "17.3.9", - "@angular-devkit/schematics": "17.3.9", + "@angular-devkit/core": "17.3.10", + "@angular-devkit/schematics": "17.3.10", "jsonc-parser": "3.2.1" }, "engines": { @@ -10925,9 +10823,9 @@ } }, "node_modules/jasmine-core": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.1.2.tgz", - "integrity": "sha512-2oIUMGn00FdUiqz6epiiJr7xcFyNYj3rDcfmnzfkBnHyBQ3cBQUs4mmyGsOb7TTLb9kxk7dBcmEmqhDKkBoDyA==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.4.0.tgz", + "integrity": "sha512-T4fio3W++llLd7LGSGsioriDHgWyhoL6YTu4k37uwJLF7DzOzspz7mNxRoM3cQdLWtL/ebazQpIf/yZGJx/gzg==", "dev": true, "license": "MIT" }, diff --git a/firebird-ng/package.json b/firebird-ng/package.json index f36d144..be2afaa 100644 --- a/firebird-ng/package.json +++ b/firebird-ng/package.json @@ -13,21 +13,21 @@ }, "private": true, "dependencies": { - "@angular/animations": "^17.3.0", - "@angular/cdk": "~17.3.7", - "@angular/common": "^17.3.0", - "@angular/compiler": "^17.3.0", - "@angular/core": "^17.3.0", - "@angular/forms": "^17.3.0", - "@angular/material": "~17.3.7", - "@angular/platform-browser": "^17.3.0", - "@angular/platform-browser-dynamic": "^17.3.0", - "@angular/router": "^17.3.0", - "@tweenjs/tween.js": "^23.1.2", - "@types/picomatch": "^2.3.3", - "angular-split": "^17.1.1", + "@angular/animations": "^17.3.12", + "@angular/cdk": "~17.3.10", + "@angular/common": "^17.3.12", + "@angular/compiler": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/forms": "^17.3.12", + "@angular/material": "~17.3.10", + "@angular/platform-browser": "^17.3.12", + "@angular/platform-browser-dynamic": "^17.3.12", + "@angular/router": "^17.3.12", + "@tweenjs/tween.js": "^23.1.3", + "@types/picomatch": "^2.3.4", + "angular-split": "^17.2.0", "component": "^1.1.0", - "jsdom": "^24.0.0", + "jsdom": "^24.1.3", "jsonc-parser": "^3.3.1", "jsrootdi": "^7.6.101", "jszip": "^3.10.1", @@ -36,26 +36,26 @@ "phoenix-event-display": "^2.16.0", "phoenix-ui-components": "^2.16.0", "picomatch": "^4.0.2", - "rxjs": "~7.8.0", + "rxjs": "~7.8.1", "three": "^0.164.1", - "tslib": "^2.3.0", + "tslib": "^2.7.0", "vm": "^0.1.0", - "zone.js": "~0.14.3" + "zone.js": "~0.14.10" }, "devDependencies": { "@angular-builders/custom-webpack": "^17.0.2", "@angular-devkit/build-angular": "^17.3.10", - "@angular/cli": "^17.3.1", - "@angular/compiler-cli": "^17.3.0", - "@types/jasmine": "~5.1.0", + "@angular/cli": "^17.3.10", + "@angular/compiler-cli": "^17.3.12", + "@types/jasmine": "~5.1.4", "@types/three": "^0.164.0", - "jasmine-core": "~5.1.0", - "karma": "~6.4.0", + "jasmine-core": "~5.4.0", + "karma": "~6.4.4", "karma-chrome-launcher": "~3.2.0", - "karma-coverage": "~2.2.0", + "karma-coverage": "~2.2.1", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", - "typescript": "~5.4.2", + "typescript": "~5.4.5", "webpack-bundle-analyzer": "^4.10.2" }, "browser": { diff --git a/firebird-ng/src/app/components/display-shell/display-shell.component.scss b/firebird-ng/src/app/components/display-shell/display-shell.component.scss index 79a6eb7..dd1cbcc 100644 --- a/firebird-ng/src/app/components/display-shell/display-shell.component.scss +++ b/firebird-ng/src/app/components/display-shell/display-shell.component.scss @@ -15,13 +15,13 @@ align-items: center; min-height: 50px; background-color: #2e2e2e; - border-bottom: 1px solid #ddd; + border-bottom: 1px solid #222222; } .main-content { box-sizing: border-box; - border-top: 1px solid #ddd; + border-top: 1px solid #222222; display: flex; flex: 1; overflow: hidden; @@ -49,7 +49,7 @@ .divider { width: 5px; cursor: col-resize; - background-color: #cccccc; + background-color: #000000; user-select: none; } diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.ts b/firebird-ng/src/app/pages/main-display/main-display.component.ts index 468f183..24a482e 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.ts +++ b/firebird-ng/src/app/pages/main-display/main-display.component.ts @@ -540,6 +540,7 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { this.scene = threeManager.getSceneManager().getScene() as THREE.Scene; this.camera = openThreeManager.controlsManager.getMainCamera() as THREE.Camera; + this.painter.setThreeSceneParent(openThreeManager.sceneManager.getEventData()); // // GUI // const globalPlane = new THREE.Plane( new THREE.Vector3( - 1, 0, 0 ), 0.1 ); @@ -663,7 +664,7 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { //console.log("loaded data model"); //console.log(data); - }) + }); document.addEventListener('keydown', (e) => { if ((e as KeyboardEvent).key === 'Enter') { diff --git a/firebird-ng/src/app/painters/data-model-painter.ts b/firebird-ng/src/app/painters/data-model-painter.ts index 88650d8..f498525 100644 --- a/firebird-ng/src/app/painters/data-model-painter.ts +++ b/firebird-ng/src/app/painters/data-model-painter.ts @@ -1,5 +1,5 @@ import { Entry } from "../model/entry"; -import { Object3D } from "three"; +import { Object3D, Group } from "three"; import {ComponentPainter, ComponentPainterConstructor} from "./component-painter"; import {BoxTrackerHitComponent} from "../model/box-tracker-hit.component"; import {BoxTrackerHitPainter} from "./box-tracker-hit.painter"; @@ -40,7 +40,12 @@ export class DataModelPainter { for (const component of entry.components) { const PainterClass = this.componentPainterRegistry[component.type]; if (PainterClass) { - const painter = new PainterClass(this.threeParentNode, component); + let componentGroup = new Group(); + componentGroup.name = component.name; + componentGroup.userData['component'] = component; + this.threeParentNode.add(componentGroup); + const painter = new PainterClass(componentGroup, component); + this.painters.push(painter); } else { console.warn(`No ComponentPainter registered for component type: ${component.type}`); diff --git a/firebird-ng/src/app/services/data-model.service.ts b/firebird-ng/src/app/services/data-model.service.ts index 9aa67c8..a2fded2 100644 --- a/firebird-ng/src/app/services/data-model.service.ts +++ b/firebird-ng/src/app/services/data-model.service.ts @@ -87,9 +87,7 @@ export class DataModelService { console.log("[DataModelService.loadDexData] Wrong extension. I.e. !this.userConfig.edm4eicEventSource.value"); } - let url = ""; - - + let url = this.urlService.resolveDownloadUrl(userInput); let dexData = {}; diff --git a/firebird-ng/src/assets/test_data/hit-box.v0.01.firebird.json.zip b/firebird-ng/src/assets/test_data/hit-box.v0.01.firebird.json.zip index aaf76cda3a01c658673d560d80a8442337a32db1..ca4a805f6db39d67b3053fa79850d7ac9c2dd2e1 100644 GIT binary patch literal 44252 zcmY(KV|X3k_x4Y0+qT&g=fp|V*tTukR%5oYoyKlXlEyY0+s?ng@AK+;JJ;-etyz10 z*4*o!Ym~l1L1O~|0C)g(n6kFAd-j$@H30BH0}mkjch$<;jm5;#i`9djm7Rmt!rH~$ z#M;G-)yCD)VP9X_k-9B8KxF$Pv|-92W|%Q$80N<5fl!t-<;gEVfEYK%!|t@JlhaIp zG9vjiziwG5#bc0^Bh$L7jKzpdL;PElHd$2MpWN4C!s2Z!G7Ur(Po zYnN& z#B7`Ty`NK80ylUfZ+?#(_az_Cu6`drZ&&vpgG+%M`aQ3^IgTEho+3RT$0D17uO5$` z5Ll?!_nu32wVx0C{9`{*xqfZvZ*+Njwsq#Mb$iqCe#^N>@=fhctZVZP^z`k1#bCSJ z+I#(07oAwUwcU1i{Qgg*}A-RHlVfi+P$)jvVCV06)wz%`)A^+3ND00w&f(u&9P&Tf$OiCt2WCkZI5Il3U??v!OJ1Vr`JUH(pznoD zA4}iSUCdvSF}tnOHnvp{tNfl~djsz@KOu2}6{eRQ2oq;C-_z4za&k-#^yqz9C$wV} zS*6qU{775`ap_BvRI!awwF(b|A)8bS$cwi*zAN1v2Lo!f12fb3%6?3RLOIjGA2N%X z?Ak(62>2aDb6Pu~U?{ReGAc0_g{8=i4CF!}&LeD=VN)^;M;>4$gn{b)twfRqi<7yG z*nfVeheIrKnwxxc<*a2-0(2b!;z_>R@(lUn$qJF9YZ(*sr(3*>!cVU<-(zq{Ot83n zQ=={#<3%A#Ae*PD^vATB3rg6<>6w(m7l{V903pkRsmK;$;|%8;cr5|Y zCJ^C4y%)Lnqs6EeM^UP{a@Fk8t|oBLtgUU`8WnA6(1R`^s%E7oDu-Pt!szqK==n+g zNd3sOywGS_0_te-25L;Hj;H=wk)o0VR-`(sih^BDl(JDr+`|2g`>5Q5TpTrjbW+Nh z$sgUVK0w?+cr-S|s1X8M_!*N{2(2BpIQCXpbKRuk*UMwAY%V9%)V)f8U~B9WwVw}p znh<)SnF(MG5>+Q*mne1g7 zi?31SR6}!s#v_P7q40rfz5GPfoq6b`WRhICi15H}=b}|xQ01&@I~aFr*drWPMXpIA zDHgaWnpK!j>u^5=(hrb>05?>yVbLFU>SV|qG$RNxZA$sB zNfDTOL5j%~hFY99ut_ec3Su&{wq1E*BeS`|=JX1t6Z8zJ6Kl-qz|OP-4s=02IhZin z_pW1bg-u-JXK8{t9~8O-omm6b(;`*YCM&#xXEycZ+nHYD;p~%%-WLRaSFw#SU^J(k9z|IV4VOjo%L)C= ztR=MQH!|d(z$+CK`1JVj_suZK)UU#MjM>eorU3vGN|sdGtX#ekUg*%LMIh1?sHp zqPDCQPaLSe-c7KrUyjYg(%D}fFwB<=WiPQeOO2PdobN41wUcl?|5no zE$`CdWavvWi5!?PX~-SzRuRFW{T7)?p3pAuD7z6!BQP?@mHf#H0BeBqj10KkEB*!v5VmV6vk{*4T3pvlq-R~%WFW(!wy6M@Q2sA)fXP0OGY; zLM-PLjGNk{`^4NDymUvBmw6htQ)T&lhGTLntT3V2|JYEJ-aICh@Ig^yPlsej4bj+w ziBM@?w_PG=aD|gnaphbN-{LBzG?$kv2;x_)#@x|( zrYnbPyI4f{pG2jF2H>GF>W^Xhm~JhUACUJAzQ0@xe|)RTSP`=JMDB8Slwz z$kJc>B}Q=Yq(qyExIc!2#n3aL@6dNMr&N9$3QX>NYyz zO+*YYJgDmCB%(?>xAHI5#6jrH^d^dZ#D02|B+ex1{bPjPqSxVPRSH(x5%76z+6~ii zF-D_aIKZD^wd>~i54aN}tP?7i#`t>ec=T{$4}$~0?KcA7{t`-?WYaY=`)-JO(e|O& zkTQF62=4ZIHE3E3q?!U!gj0A1IRGZ9JAVNJYt!24+ksed_8^x>##Y>AroL=qOFt$L zQk4=MItjB;Ju`FmxG)kQf6lf)TGwytV&nCAIFet zIY?P(&(bojnf*?*BJM8(YR$7Apr{ZY+g3JrWQyuc z9tS(y7OiGRp6ipf=nB({Rw=SLQ+@l@M1La97{t3(|KYRyJUa`a+9rpLn}^*`7S#&R zp&#xHnP4{m;G}8g!nJc_frEvhP>>|+D08p=Lja)Q7hKIZx@H%&hhVD5Tq4)aDIiWw zCXIb-2?9(ST~N#iRK(ls0A<+OdnEE5bb*9$RqV)6PD*IlT|A#nMz6=t^{9h=2)^$< z<`#48$Q0P8u?8yeC+E-&6(lS_^B!1cS}}!(Vl7SKG1l>`=9y1KFyT(8;m$}TQH7s? zxD>ot^404IXAspcCvFR~@VOv9_kv&-Zgh&X{7y!AoLg{ctE-|c{usHUsCvC(PFOaYP=S>vi|&Aa z9rAFWLk))F-~q^wkk9*{QXW(x-?L!6;7m7+#}PB-5!UM+f(Y;ir=r0fT8`RSO2nN- zepQFk;(wqoFQ@;md3>=|i_L8cdJe7N0f|ClZGXwI+|P*Ucf}7ffst;X4j%Vtcw_yZ zMic+N{5({$vzT-f*$>e05J@s#A3$dmBwDdpm-<&C&Es+EHuVKydznu!LBPx(m*=1` zt>r&WXcjG{Tn%gdR%_4QKv0&)2hU^tN~kRt4Y=2t*o#HgC_>qO zwJtw*G=30IYF;ox#~~#leRAO}L;c}JcLg;jajOf5pwpBz=s)=9e^~e@>HXP*!nJj7 zL&Qh6TQc3m`~JraoyPVDTI-jVe!Dn^xVo7>m9-)Vc-UwrRP3jNgGWaQ7zw)rHqrZu z0(>1d?%)Zd*_GO({e_eEOJ5TQ7`R2qS%|fW(Dvz-f5i_c#NI|WziCrbsbVD+*~r5_ zN>lnQ$33w)DZ@7G5nXO3#*`-G^J;2r-Q_mA=aY z7TpU!IN?1WpOD15!YYwhjg)&wV1d7!aw3}@i>~m`&$wl|m?HAT0|ZDBU4DebgS4rl zzT2JSl6Xx}=zKK`f2aC$e5a%ZqgXDZq>4}|9>6MFNZEf`H=XsBe#Q*6IN(S5=u9G0 z-o1^2Fr%w@M5Cf`D})*2!+0Y&fkcm^Bo(1@W6o=96-Ch@I?8!jk&oM&R6ijK*=csK zPClRi9Z!ZsR}u?p99Q`s^bl^wg3ZoHLk~kvVGbzOuG1ZAoR+Jo6n4Y}refn{Ly16S9JhFhE?Bw# zu-f45@8^X_Tj0yhW1VnkSNBHuyN&047B|Z1=rY@L1AhGZc;sbocgrp4sxVOnfOTdB$TG6g>wZScp_ z1UDv{Ka(&u1>hr_Ev|dj_7`B~=FdSHlhit*uE;`0hksFhbS6S%=c0iw^}pO&q(^L} zeBSBlPZ-t~!Hrwj%6fk2jD0S}-i_)+prTw50;y-K;u znD6Tae5D;S~~R8xjiG6)xO%SNLw$_bM?;cX9PT#RAf-8(Q=5I zGY-u%oq5cRw-?FW#vTy@jTyaIRl9aQUnEUsLaQ|Q9!6ikJhGQ8aoX+~BAD~|jTg0k zL1Y^4JXTt}8im};L?~9I2eD4^_kY1jV;BK#iKKt?(uWT~5*xF$_9MUU==mLm-I>*m zP)NH_> zSIMH}bX4eeT(nj@Z-T-QVaVAzW{mYVBMGXFYmjIo@M$}uG2IHpTsH!^mhVb^C*>8; zUx^|M)2&W>QVMqv#7EAm3;Cv(5!{GGgXE%GG{`!6itEEHf5p5GLHUjsCI(#2v}m}1 zg$4MN3WqRBAZWAQI{aEq^%v0~JC)li&ntQ#F^hhvvqP^Py_q2fA0xxQ7syN7B5NRB zQW4XD87@AKQ=R_9VLK?c+?QO42nM1#>8m0M2F+76wJGEInK~Ie%NX|tReQ3agrB8b zOQ}(**SidSHElb&j`)TAEeVnBK(b(-E})n2j^i?U_MfW~1bLYCO$Pc!iZ}x(Nr^{m zg06jq?t`lttj^y!meO+Fz&-RljX7vOPYn?JLCAXc&+q_*w2h;7yT7oFX;k$>qc0X> zcH{<;dWvxiR9OU;gOC5&6QW|lbbo@4JLlFHWTYYcMWmmu(kjZJJ!F&p74FF!O&KOd zFMfyGxhhCYox`^JCu;Gq30j+s;wFpgxEp%^QhvUZ(Sl<*n!U|4reaNiz&d?*j_K*~ z7rP)@;Q0Qm#OVexgzXBR;cv?&MYL5$G(&;ca?L*9LZeP1VUskGcRe`3BL|VZ$+no{ z_vUr9K$_SglyCDz<1%RS8=wDG7KK;fB02l5QWJWlDvExK4euwX;PUvjAnHH7A7nuD zvxsUylW|t(U(GS|DLvSbjkGZR4zX#4LH1vrQ79_EYJihVn@a4-DLOb0GWMTqVWcUI zX<9*cZDh^J#Ow=Y*pM`2de^z!!>Q|}8+1E8$|R~;Tqqv#(Q7_H0(x?3ZPHnA4iNnq z=uIBWC5v2H)(-tcao~Rdxd7l!4jdIOUoO;#k%9bAAZFFnp4OqCf)fY~;oF5$UAg64 zUkjN37wO-ip(cTO@=uwHx?OqiC@)#o6ofR(1cdGP0tL<@8K*Cb^T%z9zy8A#DtP;9 zR{hJ!w&W~ke~9~0VAopeVKfH;3i#Ln%z~}8hV-Nq2Qn=yNGQ=3K_@pYiGltlVDeay zPYQnu1eMFcE0+76-V@cMca{0{l2FfUklrI;Ky>cc_$FsgsC z#D{&7C$2hw(8^Qd%}H3tqQ|e^X*vL4C_0$g@?2%bB8n5fdmySawC|rs;2(u`NFZGe zeLsSdRkTdeI(6(bVm8U-Lb5O<-9Vga;5;1x^`VwW3a9xgd`&B^ks$GMKKqWTCU)+= zl?e*6Tv0B4LA$B7kj-8L_8?t`iWVWlh)Rc}39tD;MEt2IXr+dm2z`H;l$bhe`wG?c z?p?+wgpR{cKR;^+wQ_HmB~{|R4j)oc^Q&>s$p8{Q97#@wbc5y4OLPo9@Y5yO`ps>J z`b%X=jO(n8aM;k_Z*bg*19ZP8c?e}V(q_kp7gDImNm-q`H*2cO6mte^&A4S9u}t^U zquq|R)?ZAA`X4tAd&)fC80K^@C)^Euze+CJV*)i1k+u71(M2TMB;V)7nqjM>z&S>f z388!<$gF%x$Hbr5F+*jN9?5wbk|{4NuS9UJ?E%87wAgW>+HoBk(m~0ApWFz(TCGsE z@7NP2Q!ITU0=xVZSnUz9V}}KSj@U1$HRcv?eY9#L496vOoTeMGw~Mw&K@w`tCoZX3ywTa$fnzrTCiT zW1vTo1B!{K34xJ3?2=a=iigVQx6}<1XgIBu8srSM1B%A|+dr=r<#0qAE<&Ppp8c@F zE#b>=t4gGJh*Vq<_Ur_!vUi@jf1Ztl)?G2+WO7^z`bSZGI$<1X3eX>oc-6GsRX zs`?=!pCP`phENyXUx^Hgw>G$SWQA2rF77Z&4Cr=Z#!wu_?;HCHB2Hou=&3}5?oxG< zGzW{S%-Z*`!1{P!iqg~(VeF6^LJ*1QQjfddZm8du8oKjaybQ$?AaYAkOiiF z*Q#DQv$^!|gQ(cSV%_KtFQz3dBPxYI6n1}IwFGB!Sx%XeF$g5=yk#V?Z6GmJsdMq+ zYm&eisV}E?DUW>{=)F7s1J2$t>@VB;G8*K_Ra0dFkO9PaDKKmp7p?rIadFvCYk1Lt z{6cY07NtQ9KeBY=MK0J6r?(Dc$h%F_=H>5ILb!HF7&U<7hNl~sm=GY!hu{`#N)~u| z$PNhq+Eme1(346-v!qvc}h?r+tUx>?29s{wXlr2bMG*6Cpvm zk*^8$TOusBqHuJpMZ&=1{8GqK823#QvBl0DKjXn#Jd?VFWrs>pI35La1C~H1Nt+Fu zNGobGn;d=nSH%uYO;D)XUKy2`fy^JxyRola4A}5ZHu}us@ zSN`mw5oh0OqQKq}oW$`7G?N@y5RlB3t=@0g_^ zh^Ap-88u_n%C*W?(H{|;r~#_1?|}oN#0aMG7Ns4NcJ1lQA$>#7eybO;?DR|W6qMLF z6FT|H3|349Oa%g)cM57r-KsAJSn(uuE+40k6eXelAV0AQd|WaD!S}Ehe$G^_Qs!g=z+@wnEd-b8%PX=$91LhN~9=h;zAe*+( zLd||X45@2&yGR@{)eh0`M;^~Y3^)|n-2Yr^g8y478fqoepnJ2oSizmf;v$NzhvAmzW%H#%{c>_pD{%;JnPJ5`=d^2bB-{SFX z->Bu(eL|)-M^t!R$C@-qIyIxL0;+QW(HEC+uOH+y>4WUA1_=)bkomY-zNTvm9?L z!>#ER-*Ml9HBU`0!{GVZ*Rgzf^{j~AD|4V86H-Nk<=F!X+l?r<846&(Q~88~aC@aW zby?3g#*{lvAMS<_{g)aL>}Ch+j0x|C8)-8UIxM zkhHb@T(%)RZi2>CJNc0suNt>h3l5oyJ=?yODTu@E6)&x6tsfQjkiFE(wxi+`JMZ_0 z!>-ThYIl?~y#l30S}aAiwxpGW?v~I943cwX*`WPNh8B)}v%o6>cc53m zjdF~`i`Y&jw!-~qRryjo45jiX6`Ps>=-0|m zm(?$cdD!961gxFH9ZS6qH@VP(7MMKXnHIGu`ml(|mHk1+@ujW}o9l~Z?~`opfbq_7 zGQHhLfL?xt0LITag8W=m8Jf%_gW>KP-CEVc9;K*~)1}ICizac#c}+(MFkiKjzsf7( z0fh#!m91(chF0Vv@No~G&0@kHnAa1R#{?I=La$?_C#LrA4I1cd4T*@M_&6N7;30u@n1Z^Q59o8yF4N^CMa;t7GEh*TI- zf0i~odmuE`TO1=i==$h)sC3$c9d9L>m+f;RUbz`4+Fuj!ys2^NrDI+tiv?N4)9qgv z7PS6ER$1Wv%5c}bnd9XP9H%8ki@@G(L9VC=f!BNXgh(S`fL^UOw$YyS2+0!A(b4|c zxiH?^u!;~X-L*cank;G_m?)HFH^o%bPBFvdn?DM@;KT;i=)zj^GD=ESY~~mevobIE zla_)+i~H6rex?4!K81h<)rOD!{=+2OaCk@K)dtSSKx?u8unz+eyV8S^WsbSyS+7M< zJNBKw28M$9W?c$WNdSy8l)^qrSC>3jf94qVxB~np;$k=6Z@a?*DM31<#gl;#L z^UtVCqO-sVYbSg0n!c~%zz*8HDVPck=iYD}l0CTZP_M(9d{A3n-lVc*d z7kE^Y9`w-IZB4|(S*nO3NS>z#G6B;fZ{#! zUjyFW2*{x6Jb%jPe=pCQC|kB)GG-2=t52fs=HROMMrc{4`HecDiOU%tmv`=JJ^bqg zF5t1c{UJO`c}(V@mePTmc_XYz`Zwr%>;~ZRo3Z7{P8Td;3Pp{Do*jItR<%eU_fd@y zjChss+XoE_;FTa&1Gv&*(!jbVICew&n|8|@T8bCqdorrnBff&!t0wf0cH*ApH zYOzUukGh6&$#_h}ZSnB6|Sq@EOP_MsZ#QF2}s?cgQp z&#uyDU|jD1T7_6SZrX@44=SwsXVEZ>PqOMIUElBW<0PmxwLu{w(P@t?t&4#=(pu(f z0&{GbWn@g*Gb+TTq8+@(PqyO_TySiMIfX5 zR`yRY=O6=c$3$H&+A0yvm(_cYSXPD{8P+Ag!EF?lR*hSws;@Lmb^(OP6q=i=GjL$hN*L=ls zvH9=i8VoB3>z;Y2lch^6I_{K!k6BuzlHaqj((+A*-_*~LHfK^kqk{#Kr7d!*JIk-$ zZ^ybXC`aK5$km5YyNo`%u5@MyfpU)b8SEL=WscAjruEo!4Z7LZbl|?xD zk7L8IL54Ue;XNhi^k04d+6;7r#N=3vqY0BDiZv)TGBGFWU!Rq%Cy$2OKeu?eTPE;$mKo?B=Y;YNp4r`R#r@R^@8MSQm$|Li!yveLHdmzuKR#F?(5$(vh}Hb261Z8 z674&d<8qA**sS#B%ERbixC(}6sY%sDbSLBc{q{TJ-3yp&Qda4r4gYLbk%K<*ud+q? zdl7I@{X_R{)6Z0zR0_>TM!R5f<&5(3!xVZ@o&-q|4>NtMqOejdh7eSU%ZVRoqF^4| zydz}g+F39SHJ6s0F>w&g3w%$V(?}@%d=qHwVB-^}$*EA78~Qop#=lXcC0eWE;G0mK zMvm=?nL2WCuVpu^6U3&YWu=P@GNKUUERo2}s-a_#{bK76ONlmF87Sk3by6r&jNAds zWi+n*Cr~Z4rI-R}eiHbO`ipRcinrg>01fFU@@I>8TfG_fgK!I+i#^%B+!~^utcf}F zQg*O(Q~}xvhW*0Qnm=H-3wK#+G$_72K_lzjZdktzlv?OHCs9P&TSI20E#$62C;`K@ zjX)F0b2>YxE~Yk+Ft#2GKw>ndw$?LZabz0zNCgCMzt+J&udzlzX-4Pdfp4aa%KXCK z1h!7@LYT|7t2crnX}(R7GJZ0 zvu`XNJRrydH2gq8Ooe{Ji3nyG^O&7V-C6z%2^pt!`Z9?DiGd#i%PFCcQ54B7VU7BU zkptQtDlAEmX`F6)G{lye+{l!V%e~OSBb7TpBU{50M;NAZp^+sv;gi3YxE+p}Gpf{-GHo>lYR%LW)+FY1zL0gUa*Z2+*NO{DR<7-FwZ7JXl7E zr%nQrUw8o`-5?b2NXs|nQm3M`ey0Q_tqGxZE*%hKNu%b@snHMxn5~#_ABVh-y*cNk z(>dcr5v>z#HX%ft_yhM=zB}dUx6hAEMAK5#9@7tns1}`r`3Gf>$gxnqhuzFOomI7yC9V$WUwG z(1`V@eK#-DdId~#EJSeifdsAQ^niu?{Ko?$r2+~(XpVY-4~F=G>t%h+4R$2w|yNM7KA%EheFfUVH|0k z5Q0<^c_t-)kYFxPCfEJ+kR&`mdBC+rHdqz?>xj6su0>he~rCn~7fQCncc*2WO(A zaHo2t^Ch&9DJmH!zdhu2Q<0WAO%sdkPAX)^{4uSZB^9u6*DWO}_(q~-`O8=j1-{O9 ze*Ev7d72D247a?^2h#^8-eGF9=QKGF%NFkwU2$Ts!+@Gi)Jzk_Fs z7@|OF%NW<0+ERlV=E?=u7~TwUU9- z^HOu!%4&F#w@gWd$wqQUL_l9nAtxuC;#u~(0CH2j?t3ZQeazr9{zKpWX@GNZ{dZDptd~6T zJU*6w<4JKyQZj&U{MQL|toP=a@9>HrzAZpwJBLwp&08;aWsne`sWIsQ6PKfC>~P)m zy~a!k6=z{kb(@^Xxz)_|ZZBRb@7tCL7s~3TwG1}=- ziB45V8>9y%URoGV!O=)VXU?cpdfhf!O7Yd{)*tJSsF12~HjYt*OEd?wvKM6-HwJ5` zOd3!_m!HDy!F9=vb6UksbA|x-%y;jm|C+XIJZ%uM+hQoyDMhu1cg% z%t0+3)@EAa^)OA~f!}P<_b1PC%>#Ch_h06l!H5klyPAEkupGvw=%X?=pFcRvMW;fD zA5ET32F|=>gM>O)GH%QOjn(~_*1wEcGvUO$ZfJC5&`%HWYVb*Cv)?Oz3#bJt9rRWt#C3&i0+-sjVAsDd&QZ7R zpSVAvIC4j*Uzzp?Q|3fd!+gi%JxP)()=_9IGnQ<1?hLc^_?fpgLhK}UBa1l8Ayc!f zgXrKkH;UWibg2-`yo(L;@lhb^o}ui4ZMk@KAybBS@(T3T`f)J?+gzrC!BUKrt`G;> z!CE)KcjM=Gq%DBEP>YXKtOMpG8l$LPVP^9y0NBPJ7sM`e3lnsw#+DdpxH;inLMYMF zD{uc2RMcnpM0+WyEXALJ#7|nvevImk;oQw6i>mA)kYgK0CUK12*YQS*AwyB?{;W`t zV97hzyZQd47Detauu_lah930ekYMgrcaZXx8_X1>FTcN;lWPpeiRBlMH@GK?+hI9g z*;wW2Uadu0mjoM^iD=So_1Ern2;T`=M&huGg`vPt;jyA$GxyI3J4_L^D%!EET!R+t zC}Vm(%)TONm8}M~68k4XWFpQ^iKD8|yo!e;3a$yAk7>EQWL5pQ3oQ$Ip1CGPamUcy4L6t zXI5J6j`#5c4%{tB9whFj(EHs-g~4#TTF30L5#|s%kwIJ6jJal}LiApeU@+7LtEX&T z0)>H7CMrqHvl{Z%!c$FoN!_?Fb+Wzayg@T!yQ#t1ap@|=_&hKH#`GJOh=eA z7p@+C;^)3-oH@?@r}odxblFor99tuBUNY9}!V%az?eoHo z&5#Ltak9c*?fFcuvWjyeqNyT0Cf|5}Hi}u}Er)EL&MYuQekV0WDn2iVQD86Cv6t7M zCkbV06OUEQ`Z-!Ob~npxPP`_Z0HUv+hVgJ%GsnXpBsBhk#=D=59ib$&qoG(`B=wdz zMQ7X?(QI82Onjm@$bx}R1U?Q?8N^hbZH6hvv;h+~$=fE)57IU&edTh#N*kPOD9po- zMx_h)3H;?s=exE)n+fTrkZ_XMW4+jljj^BJj?G&RSHPFn`QeL8w0s;`JLeV7S6#2a zU`>J4BLpGp5^EwjpCraZPEQUVxEMU*cG>s^qKL=8EE!wNsWFB*6g|?kp;Z6+odJV9 zw$x)cSQcw7|1h1%8+KMZX<3`n!Limyw!|1lmHd)u8j`44hH1Gt|E5QqL)1pPLkJc7 zqD=55;;x@hO-KMf>!rYS>Z!v~tHXT-2NpBh#Bq;>D;Oe!a&=a(MoMKp4g9x2)l&z8 zdmnbm2%~O#6k1>h$5PCB3P=m$D!IyiIlz&=RgR-r)yBmX)OEJSW7hhcW2?(Nf_$U@$3ryt9rG1Qk`ht8~Pwp!sn<68H( zl$WC<+)nD5_u?dhY1WIww=w3m;@aQ7$T*yk`lcVT*RW-w;z5lrqWA%NE;qi}j=W+9jNYd8S;fc(+-Y{Y(#=ge|*kucrQvbDVju_NQZz+SqXPKMv#Z3d4EQ z=$0!ISs?4W*q{9(;Gah2QbJ^TeAX7BA=I~uc77<6$hmgSU~4dNY_rfQn$3z>nv~s_ zzO`V#A8S;il3PyDwf+Q1cZR??VSu+Gl9g)8i6Re1l0XnBG&t)C$6?KFIWMdFwfp~) z1F8--74UCa7FwE&eg0Uihjxa$qRP3`cvGt&d8pvDonihu(cB4U{E(kH#u>TcVD5)H zWt2}Q1}oQ17fcIyHS1YBpv7YLAtrMhwkzN}3)G}ahH>U44EHnK)xYH6?lL7Dm@W?l zcL})6sF?1qb%fm5$D@O6l|}?6qqoCz(zsmvtUV$BW>Nc8b~0v=JbcqNS?=_15~{9` zVBu3LM=XlLn$7jk&lHbGRW%YBk8G%xD3lkT3>ZYld?7?>I}M7n?IADHDwb6j&(H%= z{~Aq>iwpMU=4WD`w^ygLnZB{h@=|5mj;ku|S^M%g{NiRQ{%aYhI_+x4YvNFw(C1+& zO_e5irdruJ3EL@x#+N}oNkf4lFQ(t(eQ}DoJ>2oA+rXKVY!DX}!Gljt>n9(K7V%JQ zkCK6~T05n>EleM^=N-HrWC$gw)V6Q*p8huG=QI8yfO`Sa_8a1xm3P~c2QNBJm$2l6L!FJM0I) zQv^T243F@j@6Rml7$Lf$SdJTCVaZTCtW=g&%33tjT)E{q7&7sJvUC@R&QpAX9gi@x zg)n8gAzv=Nl02ItqzLAU8C5t7i>JzmNS2SKkc5=+tvXr14k=Cg{UA?ysM&jL3x9M& zQT#kj?e(>xo#p07>H^SsJSN1^r!m$E0Kd~AXh(^ zxQTn?v%UfZTLw21C$q~AyPmD^WfL{kXwxP&_r91IKl|H$`}KV5Ub>acJ^nG`HUmyL zEB>5L*c)MmBdd&C(2(QC>`axEsG9w75-$zi>+!z0ze#WkSGb;K9WXD^7?me2|Ak1Q zZ;u8l_v?1PLilF3=INQAvtYI>00t?Rq3jJab*PAMU*ED5MqBh;&+mYb(#}9hSl6`h z5$NtcBB^#UzAH_M76XQt*#o|${dTlyeprE~!W&=Mm5U9F)(Jg0SiIr;BT)TQ<-Jh7 z%IxBhdFcq~g8WK7gb+|TUQH8Z`%UvsXK5K)u-d6-a#`^LX>V}mLp(S~#V{H=KgxdD z$xTuU^NX4AlF}50#1o&wRYK49#P6#BglnQWP;eylBr$Hm;dxW-s`d_Z`PkxIXn$Gt zIe)HX5<0_Fw!Rb1Tg=5PCRA4>!RZ!45PDq)*sD8MGGYwW4XKsIy=2VtTy8mX-zM>f zQ+CU7wi5*!(}oPPwYzXZIlV{R3E^>i7}R|?@vn)K0%F1KGo6ffI2 z+I$m^@`~ojz1S)G*#*WWIz94QSw|!z6uP%zNp6{nAHKs#lEd}!-mzsn!J)h5u-h?e zuSj&z!7;zC*#1UgDpkK93~35HbHh3yeokmS$7XYh5zZFZaTfBEsSz;FIz4m(dnisnR5#9ASs!Tx(4rM93f zVhS}J<=fgKPRF0(<91y~b!5{ZUm1-p1sWDrF~@*c{D5FD^5R_7OI(95V~i?Zzd23t zsIxo_0U=GljZeix!W-r*uh(C4kna)M6$ZKi32uX*B;!y{5BCJ4d;hP4&S)OI48sWj zR@O^W(TZxLS2krO{3}4ZzYzzK50c-2CXl@4#GR63U(*PiSQEY!d5Vklpp{vxNyOrf z7ODB!)yq&_lrt+P5e7aKU)9Q6SShD;G`Bz8yqd<%=a?V%FYSau?dpVi8qUUGU<0!* z(F<4`C&`Gil$StwA+=(jNAFzh6Fn*TX_6 zGZo?DYEOmNQ<&Rc4C%G(gwjDhZF5QYU0=bjz&-+@MknimN?iklDNV|o6Q`5MUw zLw_xwm8byZuCc~#Is|N27zL;1)My`Rk3IzqGOgA59FG?^$g7a9h@lII44xEq!U43i zc25nwYcztD_PW{WeO-y$MYMl&?_jP690x-MO}nBZx!L29^c~a;FWyHRcmVfuHIh{; zZeOaa>d#I{ROnfRYD)EtQQExBs8OrYQtoJ57;1fgj+x{f@Ba#i+y|J}YgqWlV0w}y z|NfpV{05x^PA&aB?J+V4=uoHh-pRK9NEq=F(Y+FDU| zMqbaj8ggP!_KXwxPvd~TTGmu~wWidfAExQ%Mj_VX`*3`;l$t${^GDlwikx5JuqMVKhRF`5l$;Cx%f zwS#5W=Pj5`t>G}lfZ>;(<+rGr4#9`b`~xv#SL#9zGcbdYA~}lA;;thM?7I;iAdx;<3obB$a@(lOS-nyk`a2%v-7LTW z4*>J5sizz#VpQg(v#2Rj{;gQ7W zaPNf)15(+ObZ2A?1_xphM_z7%Dnp{fX8GO~XSBO{Vl&haE}kxG>T?EX@!kW>dhg`-(YgxN$j=4kiJFKg1fiU|lt-D!_o% zIGp#@ibF@5SR8n>w2{GBebr%njMvXK+gtgsk+2}y74E=7It`#1&NW4QNFl*nkYX&O z)^sk>j)DW1alOI-3IdrSix}Ael{5MrOYTKk-Dr$OxFT_TTL}^LiZo)4OgUC6NFxJN?LHJ0DJUms6`{WTN;1#t?IZW383rf;9DI_JyGfe~zv6y61 zVJA-fjYnW9&ATV_IkiQfHXw#fuWz&ly)!rgo3;wq@Q@P2;Xr(iJD%rm(3s>?QixeA z&;)1r*`*ww$4%V&&MW$g!}%EI5JT~+mu>CV%@Zxi?g7cZ<3Zld8n|SiG-qTHh!(SH zC9dlOFfeR(k%%F_ASv`SeZqar!2*{gdX{v~d|py>x`Wl;_)CwOX`(`}6r|3LJfCq$ zI$-z(DZwu^QMJiR9Y+^7?M(nkrjxP7fTWE~ckgcwcBcV_2Le(}-}5BzW(|iJAtK%T zeNAfc6A)1Fp|8WQV=7^oN-!3ri;~r&w0&xjaE|R-`R+U64PY0pllg-vqZ=hK5}9dh zY4bKzMJ{p90jKfm@!>(v3O+0g=(}w+yg)60-aK%s?qsL+!uu!H9rj(*iw%{nl@CjKE1-J(ghg9hRl1j#T z{6?cxNx<5gX`K$`$P&Z&hWvnKW|k?)b13xw#*prrL@d0NNggxm zl8{-Jb|ZF7Ga*BAZ{RH_!L^UGiE)eEMbo|pfEz9zb4mBsrNajmO%LIeN4Ahd<)}Mcpv?-di~-q6*^*{`OK;(u{i9bL&BrhW z0IS?h!D=Jb)vyrzMq^lGlY*nK?ky!wLzB62oM85m>Y9Ze&c;3d(Wlz383j@$?c4d+ z30$r*9m_s8OrdddV>+*pL&{a*Kx#^=o`)7wxTw2jm|)hG`ovSBxsOLi-)IebZ(}Ek zmc2YCp>P8@%t#DXi5-S&5}h0tAd@85KG#Nv1KE|@i$tfHDAK8g?Bl%ajrP!PWa6ym z@H(;H@K5hwQn0U8fJZ|TP?8RFM+b^3uvyEJXB4uDbS6_8F?&QsaLR6^07faz^Q?iDBVF>HaD>1wUhMcUaw1O_yKB%r1OyBJx0 zm#Ios*s+?;!LZb@X~T;3_Tt=@O$e8+S8)F69YZpyv#jYMrG|=pf>h8#(jG6vMN_~` zMa(Rv)>voz+a;bT=spO!4}&`q%Mf8^@$$Du@)^ z!DK~Z^r0rZ3L#q{cP_2LYHm@kQ8QdX+A;?fJm_uVcsR1aD3LkJlzl2-mu}LGU)fXl znT0%J$f;8*ltwAoIBD0t>O0mhJmFt1w{Vq^OXM_W5uY%~a;^JZ8N4%Lz2ZPVhz(F* zgMqIjI?jP!VFF7uu#LNOuyjfhiL_xQ#ef2eB{I#I28$Wqh_dZ2b_Bvd8JN7}YGp#B z|EjN8dI}Q%@^Z^-6_6#zVZ(AlCrmx{rz0P)IE;_+`nig4XNGsvExg1Vte|NsrxFBN zC}S}a^^BfWqvFy6EamT_^vCqt^H2h6Ez|!OAncX=72NF9xI-*#5m#Y zF=PVOGSFkJ6KY^?ZEx9>jC`*1J>8U>-`^D02n>`V;{wQ6Zl+;d(pf}E9BDg8rH89i z@rHEn!fpGQ10`)c0%C1sR%v;&IV{4%03hUHT68ggfIlqqt1NlvEUD9^&p;W?icI{Uj&d0C@ zG+#2$w1RiwRGKkmQU{z(6T7GI0NM20ZOLjBPa22oH_RvsC4XrP6}JGO z=mH42hM)kF1!@}uHgH#LtV;*q0f?DXN~KS?4etf33wv%(_42~iiUR45B1_W5wBjMH zsKeQd^3vb}&BtuW2k-#$2}?yJ=Si*cz~60a4<+C(M6Tesiqz@?YgmI&{xIY=TBW6>-`TzX&&p-Y6_dose<4^zf<(Gf^^3%Wm{Of=J@a$Lo z@azBly?^+RfBVPnbNJ9VpZVJ_KmW(ifBn<#lVAS)7yt6}Pk;EsZ=e5{AAkMvn}6m% z`BnexWWL9c0a60zu(%}5k(n_SKGmD+O>&C@VB=4*ln`q6#{1wq4d%i3yIc-{S3o_7 z7NjxJO_J2ab*-U2P=K)*+^>jEwgC6Fh8(2Ry0?VvzqU(+Q&zEtJ}prJdHgJfkKQys zqwKqV71S68y1J`5_&;G`e1#Uvp~z(NSNcI}c8nes81Q#@pN_n%F&vRvQ~l;9L`#iV z^jPG+F`f;Nlk7o!*IF!;+amZBJIIYwD5}<4XLJ(VI)}r8^yM$^#RB19!%DWqIF5P_ zMT%CAS59_5Jv>p+nJtLzY762aw|*B8E2@rJf8S^ei;zjs79=A@E~=c-8um{TeymrH z3>E3xS)a-pjvU&>xM7K1pQDl_0{Nxh6w*dh7v!*uPMR4?pV z{-&_R3LuN9l2a~$ z=IaaeQ^+{t^Fi%VjPP9@Lu3o;tZ;hEIeNPUKCYm`c&KHLaGHrDYif0jAXH9#bPWz%uBp>bO*=~<7M6f3vQDl{bTgA-pzm*3wM zTFBok=6AZnNu&VOHQao$8UOjluECT4PVw!Hwy;Ke zDJ8;1{SFoWA(cF)#nTI1`1{6*gP4h<*ktNTS>pw@r$u&xx;X$Z-?SAcfr92hEv5J= zOHYjHzgwAqqd73S1XexE)9}LhZ-DKue3JJwhpYk6LA~(nXG-3hyR#%A84}_NcjLCT zr=tdSf;=~e7xycYiZwz7Qi|yC3V_Tvp5xKA@AXQgzz(BP@o_U_W9NWx=_Euow?a|6 z3kuWSIb`oXh$&`FIvhy6)*JvEL&CeuUl~Y;p(3TAg}Z{>t)7^}zVi)u(+HyU4uS*| zxz`1;Vv!uvQVWz!5z(Gvj!O<)`eBhu(o0JhyPAUr4uH@ckN^tq@FnK3$V_CRPPhJ+ zS&ZK_`zr91aAs*X?IaxB=uu`^jACp|@y{~;C8Ob7hxrsts%@GbWptFoh zuZh-2-_ps;dn-sD~i*LOifD*&VF`cfWR}GMH4AJdQ%f`t zwMw@gg*!?x>C|kB=Wk`ATKuZ7SUgZ5!AzBVBN;Y1)nn9b-m z3Tr^tRn6Lvr0LDpu*B4~^@UU7$Xik|vWDt7b^M(<8UBWWVqXYJu7dJRKk$fuM#KPd;5k6 z57aE}0$YsdenEs-q&?9y31KR8uGI|ZT%5$%Io6l9Ew;@h62fDjO}kNDT>u@+jkW=) z8X#M;@({OB2$JCr+KrQ8c;hX!7x)@>ZeK@A%KWA$`2JR~26=}V?)s_GKFuX#V9pz> z-_$k+AKDfq)L2|n>8g+XcncU5rEznF|xQuTd0-Amr^dk3QHiyY(WbkL-GjddzRLR*9yDX~?nbzFEh+zt2*4)v5Y z5$xO6FTfU>cBYP+txR~W-e?Rv+r`J?y6jR%DYd#y3X zDtJ*B;Dtr%&bfv}F}0RlW_035ft!TaC@(2bY>OQ|RPD6|?mZ^CMr>;Z))q^l!1Dn) zCgd`8_zQ693kPm2CisTf#$BteV}uYdl&IdhN_I5{s8Qusnxiw<| z4WG&uB*b#LV_2dM0rZ#=AJl-66%JxpQ{<S}3YeivP=Wt5oksxC7BWmWx58nYadmRveuRcxb1wz~JJvd!6@Q~GERjkaQZs&s zAqW&dhnGk1nF2;a$ARoOaSrZN#RJSyMo^gxih?QD^*yEpevD zg_H9$X`*d&i5fETR7-cq`2MDlZuKUlt(5dg4`KaFg#zy+_PP}s20ws01nr5S;pi;3 zH3Yom)+5BFn=(smMku-Mfd*j-smMgPoh1fSt4U9^Ff^E5(SqR7JB5nFX~H!O2@2k5 z3`>l>9^ygzYUc}!C{3K)~|j2f`^t*V0ThRkG|oJw%~VpD$^PhzUMn0_8mYD&YMkvCex8dWCT@?d>2aAF3zg@VH-H||tR zVBg?C;?LM~z3a%k7TKQ^{0>b8zd*`VpzQDjr{LLKffSHWjqt?IvY`b?&dBKY4t&oj17CnLV3YtE=RuZ>=iPA(B6QVzJuT3=Ky zZ1@RY5ia#qFdJ{&?iwa2MJ22=;o+i}dIgW$BGd2=4`?__xIUQugs?Bg09!%gTjs_1 zZ5_k$PmnEKqg_q;kcdk!$)$LmJZVCZD9uPGj^8;dQMX-9L4u#<=VE}*BJF}0u|~;} zrto#_f5t6wsD)$*4nPcj&wliwozY_qp zaHz+VTMs6q)7Un*jA2r0eciux?FtiM0<6!4u(ggadJ0Gx*ul;S4!{YYY9)fJQp9sY zZ;ALQ7g)j~Ac4h3bww)aU7B;;gXO+ZE-BvC3_lfQtO1d`TSIa{$8y1d;}YGd0}j7t zM0cHbAOvTT5gxxL7@sO8;1=ZcOn2)2EnyY6PyrFt5uwQKJhlcTxdG>$vvAPn)3Nc8 zm#MP)oD;sOs9(^AKoq{06&e>$9Wr{@zD_@^PUGZ!biG9Th4j^%R=KPxNGJ^eRjPvR29u!3K}SL0E= zC|sb`IGjlkAk3Ij5_ktk;2e{1?5ivkskwGq18#!gI4&qStpOk<#PdnB+(utVui7-T z5uuXPbQXTwAdvzGeeg#b@PZAg2HtK9_aeh1XBWuq45v^u*EpMLf+f_9)2{Xc$>+ZK z(XXF(%(qm6GN78o9)lQ~1j!=EnzXN1kK6(!#}ZYhtm0?lx|zpmbXs#%s4`qV~@1kZTF3yKVDgb9OZ5Zri;Fa?=Z zlM*l#i_z(Y@z|a9=h&!f1bEeuTI(hK>osala7qD|kk47ZnVNmJ(101>8xrn@4tZa< zbqzJJrHmI9lay5I1x2SNCZP|*3nU4?X?cMTnnZ4Jl_gVGXC_j&FU2Fb3|WjWX}(?d zMr+W!iZP;btB4biX;f(m^%cow!?DMTv=YFfel{876yP%%C$i?gSM>hY&_X`rgmHyg zMZV)X-pv@21RNrA=wfw7aa*ZN<^UHC_>{)W*xObhsp8r*OV^VV_5BTDiCVM@J5Ws% z2i`S~$K4J|DP#d`qW|)VXzRW#Zpfs1T@JXx`&{4!L1K+LCpkI&VnQ;|-{Ls6RKDmt zcm#qP8O^q?A%^08{-<_qXJg*O*L}tExurTX!UPoCvx%`akS`d&vch?^i8AHJWhVY1|SC8wg{;du@}R|ub$gPU#?dnS#|QPKJ_5m=9WmMRQro6c&>7V3Z2}uiAJ$M zLmK}b5AtrtKq3Zwxll-lFLBa$K*nMDvlbvD=M!Jr5O|{{EKz0PGO;TT5>MLYi-&?D zB?fN>0Oj$d`y9QNeN7>1y%;_&VGEi3Bgqh>X5((f0!4`6xl$^~D26?nZLvZNmU;uJ z$=SKz)DDUUWsrik=W62`61OSw6JZD0=7O!;nu5sn%MxpJB+LSCeADWdwG5Su0hWSL zm(hF*6n5@T<&caSWvYmxNG2|b4{MCO0T6m}Hk?Gr-85XZrED#JAqCG%wNFLl=E1rl z;K2(yiY@?+OH>`JQKYnFmlt|Y89yNxGQ%R0jhC`_?pc$(s~3UTiBNck3yfinGLvfP zR{zrA4su93$zoOOSgUSZr%)Ihd%k~Nfl2OO!5jQeNskey)KY^q{Y`{!c;zF&f@m}f zV@`hzh4) zo8dNhYL4NAO6k0$30{)%-gLjDL)e}06V^*Ad$H^BuFSz>XrCUHM2lwmSi0_ z0@fj~ydZ~IW6?Tku$buqwR#?AinRs2TuUjsq&QaS+_ybxL0S(F3EyDZF4&)2LpdS` z%9FvY()0Mj6V zM9Hi=jz>859fVilq(Zk)U*JuDqb=w?ndG8@WlaysAr8f@XCtCWIu|2`XWq_z@E7>e z_cewi!!44%?9yS^6&CKzfeK;st)pWd1_?mgK%MPYw84?Jq5s;pxNyjAgr#``tV%y$Mgd}iA+(?Rsu9Rs2ST zo`Ic)lvFKNoAx7wEBQ(VqCV6!+_v9%l7;oSXH>xNZwz*$zu*Zrq^}RkD_aevYKmMG zoG20sLJ1en?<~N>II(TP>7d4Q_u{wRh#`1>-B-*PYR^WcAg?6x%R}^Ka>iQ*2q@M$ z6~@#vYDhP5@ipe;+J<|C8$*T^a8dZCsx`H1Hm1%H@zC1i!R~yaa_U9sPv#h$#=tja^Xu|lhdLt7GBIO@&^6@X>j?jdg%!bfnH zTBMWNeXc3!6o47&z>#d_BI5~;*L}mx7ScV9aGo*=9=_v2-pv+{Mm-TPk}5)#moXs~ z@a2hft9Wijhddn{R~Xo=?dkuEqJ6LWhNa0DC@CVT^=13{$YbWTG_V4^!i+R}V_N{Y z>vL`mnMB03xxg0IAbk}I2b2tmT;eoOI7BpL*bGkn^K~U+5I=#H@DZ6tb#a?Z z(CljreX6CSeWT!c9yVp(Yz<4)oG3A7rdB)w8m95aOryecG=CvZ>}(Ah0G0TvO4TTO zjSSfe$>m{+Y=Ie#zaV#BQ=rKroXuIAwyiXHD21{C4PnxYUc&*rLY!bP5;mZg?xp6@ zttaE#OMzrt_$U&%HSo0%Gx~xz+rinlZe&X0tnEXv;~^wxDT1tao>n?O(Q|CA25T70 zUvFy*M?>P>7wNc@4#aQ?cwAxZ@gQXrbD5^8Q(6xBTw~yzSL8ljZf!{GwCzB2`z-X1 z>VN^Ya*a6l05N*dWC0n3CXFS@c&Tz*OYor937cw7h@t!LR0{EN{}nG(SPfAG(L;eF zqR4pPZsWmt3sqQK+;Vh!9GgcfY`LQ!4oa;J5@@fLEY= zdvj9tO@1acpDLlAG(<$|^QHOdOe0z<|7h3l_zD!fSDh=^Z^ z)2?Yo6ufPWI4Ez<;a71HJXyN{F05e<79|w3_>y$QA*qF2T368UH0fn->lyGR@$!+! zC5q44LuR?LMrZkX$n=RA0sN$ZjVFirFumS3jX3b4i_#i$MG8gI>?^Ech21G~x>if- z#xbsjjAHnz#RGg~0?bW1%O&=;hm6FVSYOCqg1q~JHsuoVk?boaWp3@<0Oa5oz#|0Z zh}(_sb3*>Nt1SpA)O%;@M)z6*+1Pj*B?>TrAk@@AR@sV| z4C-)+ZezmJHyVTAW*%!X;-;tY)Cgj{n&8+mw<76;$W6zMobN<&$ zlFJn;(MS}42(&tKMnifzT&80!wgB9a-EKzFvavN(Hs|o73fEsJ>GvCrVU0N9(ks!( zp?lgi>^R`({eSYVXxWhzSAJ0uNCE->|6@0kvU_@Nz?tr_Srd3RVqf;m+AgxBl#nt* zX==}I#8-76uguvmzO*q`DX1vk4+VV|6AZlCqI{?s?JXz}|Mat-tX$X7=xd9x05<(y z4aiG_)&DYMdJzm;tV!UID^f%(2u!7nRhcXXWJ#0|{Ygw1!?94P!TY5I%6~4KuaIHb z!*=STUc40^vEKTw!3rvrMjZ|Lh|+Z#fwokT^hm zh3rE4rV$*9&{#t?|DYUv?A3&kE_I&&!=&GfXz&Mp#9N7{j70p2G;yJXmROS1=+WZD zwE4nYkIGWC|5t9~70hEZg{@z~-@;-B<)ASK{B%oY&r6h+4Nu*^?P`M7-@W|^p3QN; zC0K}$NBqeVr@}Q`>+5vUij0aTK8$~(;hS9sfwscC^#w!nHX=qh=o12Bw7j)d??-qlx0{Wv`7KK20S#MIMyAU zA6lRk2aA8aGHr!RLr}ZP`(nIH&1Bar z(6Gae1jpS_IMYl58G=gTqa;y5DMX^nH%0~x=_ZckjVbvGb!e*0_J1;%yr=RREOh5V zuc)B!PKphUian?q{zvucW#zeG$h2nUOItYlxMJ{8Lwv*qP*XnTzxWyNg95l?dd9wf zf`f0M$PsucJOe8la~!@H$L5Ii{~U+Ty{ODN{6iL|6VX!fK#Wqj$Rf@4tJ;o=lKc0q znIWfo5e$0>e`fAaF+w3JdbRPDG{|)7_TEog@l{17w`S^Mu}6DmiCm)>jq(>iYwMdlDmO9kDIW?9!Uk(@;L6H+aTXVp zEA$J58Qa2{e4Jn}LY`C@DDh$~xh~~tl?^rBI5Wj8d zVl9-bSdb&~W9_{q(uMpPX+Nt0Kd$1}+U$kDa-(K&L2l@F_uuA9=OIeBm|3@EyGQFW zd!QJRE2u)Q<7JpjEr*iD>E3xSSC0Pb*va?=dcBx0>;Zq;oT^} zOv4M}#l}n5Cr5@`G=8DAh&`%G1Zb(1y_~=JA@2i&Qvo&!3R|b#?=Ve(O$4yUp9lYr zZ81<+zwgpL6j~MxLvJzvO7566HZRhQa$ZaG-tMB-9|>n%ne zC9wxuBPcA}YU7h8W#A+WLP9r9`>r(!DIxc=5+9If>e*+Y!Ao!{gaL0&^KgVsvjkdw zS8vlKarvpC0p(WdQjZxmbz6jmEz}tBIu~^uUowvqEC>RnMIc5UF~oE@4SXzPz&UFv zaq(pQ4z)x|C=+6{q>a~+!@3gOGUVSz=ZBZu=LUugkLaF^XXi0W@?n$Z&pu=KG?9Ym zLc{YiU6kt;2NEU)d+I!Z99vpmNTsD0m+O`*BkzqxZ@(FSR>>{@=`VRB9B_m{ei7>? zRKtc+x2O9h@;BK!J{AmAP3|qog)4P?498~+wGYSFW+rin#@pk%{WJXcRjeUWu&C2J zLr_d94%6w2VAz8;umu?z`U^7RMDi;s4ek($884VO;u@1;jc-`JU1$M_>!?`BzaVo zpa98r;?Akc6?{cORlZO&T#+7oF0au3{mj`m-OE zW>CoFNkr!Q@yE_as|Ifj4W-dnVr;ncXP>ZlWUyShX0Gx)=)pJ0VR#Ow(gb7$|D7br z0@6wkaw zprAaK1HKCkngU9Qj)qmfr7_%5=h0zuPwu0mSPO>2!G+4Xg;-Pb(eI?SOu;#WMNh|{ z4~7DGbMZSvG*nwTEntu7c?*H`>Kvy0iy!hfEC7okBkmcBgl=vuH-N(c`2zCbPvtVhqFYqUyu=lj1N#FoqL$c@M z{n&dDk+KA92nt-T(n%}g+$*W8BANDjVx1oFCNuFBIgta$$oy;p~~!d1sc3EU6o%O62qpGHhc%GA_xwY zVwmNUL&0#N0B?^WMWjf4*n*9jJ?aUGD4oEN1hggT#M9i_kg(Y9tBG&B#KymzA96a> z)4|VMFckYs@3Eyu!-AYX5ga-_L|`hkm&nKh!}mUg;t`1?mH7?Dxo}{6u6XI0qME1i zGV+UX*uu*6m+N+<)@%0hRaE7l(MBK_lv?8e(oXF|Qp~G%@AGC-K_nLx6#3~!CFEr( z=D+wMZ^J@g!Ef496l=&A6AvP){3XL*WSSRF^kUDwt^#q8a^F|sz0DNOeLo)dxV*d) z{k^RwZR%0S_YbNiAuL?MkGGq^9U$DLFR6^^$yzK}3r0KD z+t_^EY`91@Tj~am-*$T`+<98tYDfsMKNfyDHtklSip2 zD4GrRFOMY*C5ZKbSWQ_Cq4WJn*rLsFp-Swd&vZ4Pi#}@Qy`@NX!~}I2w<(XUC33L4 zl0aeEyskjQ7Sr;%(XtjHkY+bJD;b-HBrS3KE47Kf{J~~a9#Hnj-#kX%4~9MNCZq<- zt)QGi^v}pu!+vzJU`0qQy?b5{*KJO&CCKfbOuw?itium^84^3RA^QM}G30kDr6JKUp zNDrc0uJ!5Q`F0DxljMbp_2&6qj{x7K`BvZgQ3TwK>&xyy?O1bK53|s z_#ztikn=%m2*Nb+Y(tLH1Q_OR&{aSj`t{~lp&^D`Z+Blw;%)qlUPi;6B|h7g&Kg+Q zmg1VI5kH=OOF{5rM0Xr=0pt9t$`X_*sXVBD-x^o}h8;pfior@thEwdKHA6785|Yt` zC&t^M=BpwD7bu$cuJmIpcEKg~9-*NcN8e(aPr_dXMKFm?v6g2GH|Jsjr<}FlVn=+& zWVq*LC~OcIxOCnD$${9G)9~i9X8KDgs3X2ANYu9#mcob_l~2lig-q-w?h=k-BI5^& zM^6($GuqV&_YLEtFZEp(=%#@(MqD>deRt&jV6Y(Vfrh5Bd`RTWJ><+`HmM?6hoi0_ z71SvZ2cynyEE#!jT;RB0x2nq(Z;i$!-N!A)A35nH+?y!Xt53kddV|WK;&>|Dz51?A zY@Bq;JS`BrsG7zv!=HV`91c5J9ZP8bd&xBHuH8mO0D-nXtngJ>;NV-YDw7~6u2{U? zqKwEEGNK;{OLZ+nz4&g!;U4qa zcQEG_p&bg)25IA{IDg|ooMv81)E`-Xs4n9 z#kcakE%Zb0Bcks|!yY|IuOVkgVOj3@KeRg!+`!BzBMb-a^{XZykwm4d{vm$jjTt_m z7m=_BU+_?IzCtUqCO%XhKz>NblCh9UPi8uf1%idt*9j|yRIH4z@m@y44jl%;3vV$R z4!PaU=+QF=SK#tgs<(*K=obH2EX07A%{O+Tzv&)P;c7LMC`|-4M0dd+d;=4zx-dc9 z+K0pCnq$Fm*?Pg2T^4PT4O3RhPao{TH>e&=_^h4#mlIF$V@pb8QO3@QC(tt8q2G^% zJ$g-2NRo?2|KY;2)F6UlM}N@oI2Q}Zqg|whDsP4F#F8qaJ~_aEF`O z2ix3gKsnt@|650XtVZ!?pRsp)K~fGp;{u9o6T=bC#8EjMk>i#`9lMzwtm=i||F1Rt z1(oFrr6j;1w&LS%(*?8!s{Q3?QYL6PH#P|S4Cx;%teAL1!E(n2WMhP%{5QYk zePGBI6j8xbZkA&nnyZwEq^2$6S=^4`gTFbKG)Rd5>OIO5ENrkuB`#f|s6;bwGWHo# zM`h`TB+)10zDq2+8*FqvidrbSKb``65evV(9V(l^rv4Yd&YHf4OFY;*7aOF{9MLBA)78)QJ;$03+hUJ#)g=3mTK)Ue#-mcAZ)do@ox53 z0UZ{WUP4WH3Px+HiLJoFT_;Y5_QO&yk>=0uM??K}e~p(j*76xN(5|lIUh(2rAC7We z*+ViZ8L!67l;m6}l#sLDFYPJ=>^)>3EBo$RYMrl2v>+%wEpm>bSseq@$p2NCCZ~N;ZjV&=G+BhO!ve)oc)37Pzeq;I54(f|g*kjBo zdBDflF5x$8I#CV~{E~*h^c)yo(;SNg$~biad6a!!XaD_3*kh0ZR5^mOSftv3MJeOG z>)23VmE<&`D3f~P;3OgVl)F9n9Qu1d9yT#2O9dyLO3zX~ON{Ukxl6=|@w-ygeCsxqa{HmpZiCIJO6gCsZuO#k%1;EY@9`Gdu6iv{;+$NnI|1-etA4 zJ~BJpygv7u3e1W&3XI=5Cdn%%<@X2-qLriaEv1CYG7iDO%{pG%xdJ+M;wW(JY>Nl{ zzxR{UTl{PRf84@c;z9=OxAlgn>0-l~XVDyJ-k3l#X<#-sWIA@6>Pk4Nemn7#YvZ4N z#O56*qahXh9M!$iXlU>a*o#Ow;r7_M{i?vwf^|oMTpp5U?}vguZ>pq7)fVM5btMWw z8XYIMGWRCSHFUwAr_TIv>g`xCTqvWu5MPO?CCrC5&o86l;V)4Wuf_6*_@mS3SpLy_ z9krQj4Xx0)>{(5GSC*(jJ7p-Qcns1OOgZgwiOu~PJV#HWo-4N|2H)V$Bt^g(q^98| z&9N;8Y9@4T%yp*Mdn{Sj7;t#aQ9H-!G=p&U4p>P)8nvAp7jmGBDauOe!WF~N`63!N zVUHT(e|MQF<@8_u>VrGcegC{4m7?B?E7bn>xUvK_bjvZ}cbY~UPp(uTm&%Z_D--D0 z-1XSdARu>L`VpVvak7Z|A{h3VZMjko>akT0Gpo~Qme8bPuK6Q9C%oxF@cUXU;D>c6?3FWUg}O>1 z!5r^f9UB(Jx|{3I1p7rO>`_Zdq-J%?sGOqDv{)_^#>v~;P-n^v=Z?xZye;ewf?p>a z_gIbqLqRFwO;JqL=?wg(Rzu{LcC1q+&~t5wt`dB`1^O0N$AU5eFmRk-yYVpi3R(-T zs|+QF%zV<1Ltr9F0UmjdrALC-qrNTk{ieWviBmG$t081Q7{bO3>f>`2|!nUxv&6#*y!>`2Pj;lfJFp^1C0&7`3I zeJ$A;z-#Yw2{z-Kp{9DiGhBkEL#yHkJxM?(N>e<=?0xyKCH8BHlO2Hhxf8GzUQFb? zq!oRIeK+M(NR!;&Zw%o*Oub!yk^@ZLFn9HhFoK+E3Pc{PWA^(mZ%)~J09a{ zo*-%=7hod0vB(dvX;Zb(9)QbzrA&{Adis22qoK0SDPzVlvFo^g zaD9aqG;m-53IHg|I-5sO3pvAGZB7BYb5KCR&%)nCSaz@B=eU3;NF|9p%H}kelfl~% z00(lr=^GFG58Fls4<~I;;}o}LTM!g3vA@6@iJFV1d+-}xMpY`2RFoGFI+NQT%qV+6 zw38x-M_}ZAAea@Tirmu%jQn1$EMT z-8ejkqSza|11#;@8&2*bQddaNT!Q*Blm3=u16CfjqEQ8t4Ypc|rHwDGHg*RiX%U?v z;{#Gz(SE!{syLcZ!??K^;$YRG!j2+UH z8WUo2L?&%G=XJT{#%MeiFHxD}ee?LR)> z6)rKJN>pGg8tuG&7{`SUE~5)sA4wQHzQ5c!G9WOdJ~l8bry$q1?!dWhLo$4DOsA%R z6)EDakL~p)*QCb8gT3;#i*1x*U^5<@9y}8aDQY#06E_$q^iz4pn#F=?Zt^P4Svsa{e4OQ>&qRNu;Cc zW*!fx$qdZPk@R2(c4Kege!RN&D6S|i#(diE^oMKgws;Cj@59J(L7}KRqGih}f{LOI z#U(cO2BZmPnt&r%26SknPk5&@Tmv;A3kFTm+MW1!^q{#02c+mqb~8nRed`IakXr8+ z-$c+C3?{A-8EOoG$P_Yeb3WLJ?^@v!nbXSgHp9Mcrx;Ls_g=AZ%I~_M9es^m21mAh9 zFagtV`+vQ7-5}$1s!i1y78fX&+bKRmog{2udEyzL{d zeLkn8>~jF*in>XBA`lTnO;#)dJg#J(v4&t7}Qx4k-{#2ipb=4@=FQFdtbqyP*F zK>-jmO({9{%@L%gAqfK@zGhT?OXk!U2n@pQ3=}dk=MD^q&smr;g^RCqqMm)(!6`h| z)-pU^Vlr7B(TRf=;NiL33G{+)yg+jxZ@GP#T zc5bweEVC3KyA47}AwUtfb(XQMIAmK;hXcBFJ=aVP|G&SUR`GclXy=fX1Vykh=_7(+Qv3 zp{jxV4fy9L?b~au!yi-|T6|G?jo@%(Du&>P%p6L~uueJq|F|?BhR;#3x+6sy5a$9mjM$*iiq~Gx%&+`V@S%r7AZNB7; zM3LdT_{mB4MSl6$V&!PTQ$MfG2o5|8c)l}S)WIMKHM3Aw|Y*+Db|O1rn|!5&9cBSwoScP|>~s<`j4ZP&Y&cM2F31hnC?rw|8SgitV4|?d zcxXv#Clr;L0z7$KY-}40Qb3sGn|kX#u9;F@0W}`Q0!jx}DN_x?Be!6}Wm&jPEn{f8 zZ?}o}0lnPg)gdK$p6?5nP^UW_28=wXE8Y;~nq8 z?ACY-XOQTjz#WRGlP!D<2Q>)kUtL@36YQ6Y3?HB}bCF$!76Y$h7y?gO%3+$IIGqA8 zmN+dk_H_p7jn}yNX5td_>!f&8e1nc-@w7wGfIo`dYH|2Yn&{E&TR%jA=hIeqcE1(g zaD@w+!yGx0y9}r)r`rr>q2*k%w>;OR*%mh7;m?7A7bMCyd|}?{4A6BM53Nh))_3o<`14V$fp7 zrHe8@q`CE&M(j@*3g8w_3wYG)tHC%n@WE`b+RyUWLv z(o{~!Q>gd_TJMFt*nYf}5WP2p){(pEeXoh9DB7ZY3eO)eX(Q{&GdavOA_5zerpATf zS7QS{$a3Y&OUes`g##ZAuuxDa#;LRv7g-|yaRJ6PYFECJFQA5CYc~F-Yy5myxJDaV zl8212l;Dxv&>s@FIc~gd+gwQNY5p!zPZnn<97U!3DPB7G0?LC9<}j2~ z(Y%g+?2UIF5jAt)Fi_N#CFTVQ;u=vwxxyguf_^SDp~Hs1d@H*&oJMS1L!id%EywKE zG%liQIpe~oQbgB|98GGLI%}sn`8#@a)V{wk{4us2QywWGEOciuZ#kziMe5r=W9AI$ zzQRy>#MxXvBS*Bz(|Yyz00`h+KqA2p4um<~f7;g>YB23pgk^XBM$dPJi%>YZahtKB zY`#&$557U)W~r5m6L1NS!>;7Uh~co%I$~IOD{~3-m^q86eBaHistw5)QiI3f$Cd}O zBHPvxyrMe+6VQ@sTyYt3i9I)Mi8OWz@4V(IdW_V-hQ}5F-Py+czFC6eTu!+m7kA;M z;q#s08r#q)6TZIz8Ab*KjNlExsXiD}Zd*(+OZ2O$>M@b9bwQ8e8inPN96(mdN7Lrx z=$yF%n@LP@YLUE>GiVN}Z#~x1JjW$cl_{KBm^C5x82sT=DoeprI7NpT`o{;Gw?} zJg%W947=6)mJ$WYeDTT5HTL3#i>-XbQd3RiIUJ89P(zrBc9Q6!-oUoLK;kAA63t3R z!M8%={2H!s2xSNX7D}wOmI<+E>di5flh%ByVz+NynJK)~>wm8Zpxdr6XYlzdP{CekKqceO(~VmBBU?ZPl$Gti=k{Wvw#F@vjv3S)1;qmO zp^=EH&G@LJg%Z?mS*%KE*8ti>+o< zm7F^aVX{YZr=zSqB_&=AJ06B)zI?su3b!(cx(3+T%(YB(i&1XiJ7_E|QqXY5{z76jM#?UESe8Y_yZxoxMRP zklfWFs8f1E+2l?(*x_mmdsAd;QEf#`$K*cyrVAe6_v$hMMQ8Z^5^uObb%}Zrzo$JE zoguXkvPt1k|4b?`**J?VYz&R!1LKFNaT~I|_VlE*~LzS%5)FJirc=6GAQjx1C)Dvws z$l7+>P5N~aEmaT|>Wan3i}WAGQ(!odb1*w@M;{HpIu|bdytN&3ikDp@!x4j>o8ys< zCnVDa`0*NLrFbAIQ_`5{u>pP%04o5Xnx;wnao;>aVG-|ZNX{cmT_I4XcbOTF@B}UC zQ`R5V5F1EO)lzLUaVf4sN%6>i*f^7t*w`C*AV9Fsg`FgAsQvp+Z+K?&@!S9U%|H9r zOTP1vAO7;wpTFR1{qW;I|KlHj`sJ4||M@Rp{`5~j|N6ssAOFLz|MOq{um1Sg|Nilp zpZ@e8Uw-+&fAiO0e*U+g|MKJQFTecjpY6-ffBM58e*61>`03Z5{^o!4??3-KaL(W1 z!w2g9Kms(N+9};o;o4>{Ds!I3!?_p!1|v@0mJI^Ac_bjXCN9Q}oaOGfaI1>)e1t>a z=1)_gE2U!(cFVJVm zKGrX#uUXt*iUlR_*$b0$Lo@}|q|1jlMvu}jcD$6^n%(f;uQsG}2Z)h+Cb`icHQ&Yj z7S#J4Ps#{}aRrZkw|+YJHyZgZBU#+u_#jWTegvdG6@({kt%Id7{i58cwZ?E4=Gw&g zJ@R*}#Of&dR;b-{>x-Ve5b6n^HDtA1Yy6DYx)kZ^PBx)|IS}wqg+#zppKjmeZ4y5Z zn>*X+x;go2eLt;_oN5TH!|usi^OrvS%UW5MOztx{9yPoIQs4j#hhzEeZLFU|jc)Qw_ zs?pPMX0X`$N_EGWQRkm2ee|TMhGgosmK#gl_SQGfGHR|BzerrLEO`t3=W!;$rSgFO zbKZT4C7$$vlL02hiYiRf@_avTx5)nydB2_0Q(!J6-a4e+T5A2Ik&r)Yn_u1?IZLpb zPx_x>(>VJkhEFY_*iVlxPM=|R&9{V~aw3uU#~1pjGI$_Ra;luAj#GZ2009eMB*1Lr zHZV^pc6sgL#pzsNUw1-tiOm+Id<*j1w(|t-b3q=J;3HkzqqJSDkDz~3C|tMun(jMp zyjHQ4f6sXBY1Yq`g?^%Zbw7<7z1F;|Zvw|?(Zk;_?n4qyJ6<{!|$|x&-o<1>ONMSkn+P* zehJmg9SKhJjEoErz6AN&*dM?4X&2LrhRF4bb!RVUiC1oHeT8~eWk{mD)be))b2fBNcHKRoOo<_PJfm}ttg{C1es zKWN!!|>e%?`>NpW|t^PvRj-!_RmQbjL3>LVlR7aOG{{JpgwO zy^%ltCEjUrzpWn^ z^!;Iz_ST_gvY2Z7@SYy*O){vMr|n!off!5?Kp#nXcczqVa0&w}S$al2LEUeh_fEo`ar8upD3@-)jI zMQf;JK0*~6eNx(!3X7ikRa1QQM-n$$*=#-Jv*ls{Udvv5J*7pP6`|Db$3whxdXJx* zs&zWs-LQbALMD)M{Wy1o8@+aGXZ-g z235pEto{=XINcI zzOU;vb^C<%T)~4rRO3YvprN#y;sX$<+oX6r>&Y0F3cf^HcaP<_3-YMZY8#=*#fIlk z83kqq&}3U@D4vF}P;R1&lYFq%f}Zw)3IJ`bGNw&<+b7ISe+fzv)GvV=IhjNjrZV~= zkBmRc86l7=gOj(u$S!YvkM|8#R;f$z9ZLFce-!hQ>u?8ft?8oyy+zp5)3k=^LmTes z6xTZ2)hOHS=bc;G{%9-d8Ky@;)lFZk^Zfq9n>j%1LA&E}Txp z>7`jmd4=2Qbw0G+s-u%bi*|yaB?#YdQm9Fl(jf6uc69KuL6RfdcUsWd&1DP?NF>&m zvHVG~um;~>2pH~-t-)$K}dBj#>@Nszk$fnQ+YH4-CO z@51rL?%St|WRK5rNU1j?N)7hT0JhJJ+-j+c^X6|c%F>|>lL_viws180#^ zs_Dv|SufJ0UauaV`K8J^=u%Q2I9@A0Mf8pO(5C+q$LpK56;Y{lr~EjlhhQkhngr~T zO>H*Cw21uKsnktAWP7MEgGHvwzrkw+TkX^vUSHE;bo#2X9{fR|M;hpV)(0!+q1@G0s?R#=H`rv}FAwCVOTM-wMya9>G&cmJ}dTQXg zB6{LTE|2jmc&=)8C1vQCF%__Vosz z%2FXD!7v)t&~r~Ns_~PZCEK`Cf=6zr(s7E|D_XwwZmfEKy`$gMzuPFTfK4NLj=k3y ztfwDrT6kQ|(+5~Z@Q~aT5C1CcPny>u3k0=iVMN}tX;AMnCWvh*s~3snxQfWLlY(-h zNqr%^nh@A?tKFN2jfU8Ahw#mLqj=l)8AEB32!oke9UAvh!=Jmw z!5nUl>@|NR)3wVjJx7BHLwlD)$+JpcBBzYQ01$8)2OnK7T~zQfmDz>!MIRoA-e)6) zm3u6H(5DER&oz3t{@S!e$G<^k4t)GFNu8ei=_|I;Rna3LVgR}aDwDw46h7E%9F8BV zX1JJDMUwZ!)Nd7=uZOb27JEvd5$_~K1#d8gCVs;qgc~Fu)S6>P4QFC4=Jbv|i$<Nkp?AK=)AKCJFk_?Bot5@?do;cNG<^9^iVEgD@9cujf@Q>82YWe$IEMyjkP0ZVhOm6R$x{HUjE^n$}Dx?$iT+_v+nd&Ynbby9$TcFYi!v z!FDZSj}8M?viF~A`Z%Gg|00o|I7FKv7*3)VtRxc)p)U=c-m6%vvS(FY;3PuOZLNQ! z?;jUWPMP{H?SZ*gMz$0g{RER)b`lEj^5FW*{0G@eMHP7V|+Q zD=zVn{lJ30q$4?dg7BRad`m)Cs-2hfW zV_?(I0+HW4_e+)b3iVx9ZAW|H38`LkzTSdBLJT7JqEe*=PQc(0JU#~XpC~*PTE}Ahv0HhMVE0G->^0{De!xgm+Nfc+WKvgd;nHc zID_Q6O1`48>tjT#pWU#FhvbAji9jy&_MI;fG=I-3e>H)15wmkmTBehy@1v5?PKL;b zP}97CwgR2P<)_|ecfRr-48YQzReq9a+!HJr>>h)4?0vC*vq1_o43R7*e@U z)SU~SD)~U<;)kidUV>*Jt2R}bMK6Rt+(uevVvk&^I2eh`^UezJp^OBpr%#$ir2d>vtoA;|V*WsKHxbzatP{Te2`L{5ayyQs6A(Jzb-}jPwmM+9cCh5NX!f4pM94>QkdC+U3%Zh8b&V><&i! z#^lIIGQ9=`Hfzuoi7eB`18`n!q#|gHuL1uI*d=t0`qUuFF{_!m=j@w1SM$a&=Dtm6 zMF7r%f$X~OEW8+(g{uq{LVzTU!{LD2`US?sQ0#R5|6mf*|2vg!k8Fx9Z1#Sj6lRs zU&`~;aFo$!Qa5wG=r<1Jl13Zsv5x6?z5#PILZ{y#`MNS$NcvJ}6FftX&>c@aL-yGC zZ<#gN>LdEi6@AFT8K-f|6@8%_>m!mL2DI9vA69tsN=d#ixgIsL=Ly07O8_kf1eZ7^ z852K}I&#z_psrzuB5)n6ADv_>2eUP}&li1Ym0hpi)Trd|fm}i#oWbd66h9uq4LLg> zeE7<((R&-POl3DXDqAy#Ih<`F?R??;+RAH{-C%f*aD8dF;AJCfhQ3p^jhgjpI7mtJ z9?g5ZI=a-E<=h-C^J5*V7Ny$0e1+lh1LU?2WeA|F(ZN8s8Bo7M5sEYI5lv zg$>qO%VbBXG;$MVq;3p;hVUWPH4w3IJNBP-vh*QaV)G!q?_;b|tcYwFVaTwt8Mo>h z!bisye->(&J=+HO<%9Z#=2$&>LtE?4a99yYPr=u~pmWVK4j(iSPpQn*B*RIVP}e1w zYBwhuc45X_YWhZss^{J&M~9@Swwptm?nE5o?JtDEkemiymVjxDpKb5_e96OC_H)vw z)!W=6dKPvm3O(?$yV*N~QMEHA+~G*mG+>!??;Uz8?^t~ok>i9M|24!bK^+q$<_Ao zH6##eyByu4zl_Tlhg_mC@&Sh25l`LWr8jh}U?0i)wBw?!ZSffJM#YxzH+%CkBOK(U z&d-Oel1W}B`23pUg-V3Y`Y8JPo_vneB)^e{wZ_L9c07Iv;-bL;N`w&@Blu-bJzqql zfpyp4aPBnR$1cXlY~g3)$t>7UBTC#5$bklg8uE`Od~lgo9@_YNDZDhkylI8K%DJr3 zmI0pO^(NoIwm?ID^ca9`9i{UPY*L?zHT)eDgRe8QX`nj=Y%56x7@H95RpDbR>*+0g z>PK2rg};G{-@LeJik}K+Ugs-pQC~SH4fF|%;qQa2F4N84V87I_q!PSL4 zTl!;7B;gjgEvv~<^6J>szl}&VD!G=%y=|(uf9#hD_egK<^f>N0xo;GHwfa;wfW3j8 zVRtBes_?UsqJ!Pumt?k2ofB;ZKvu#)Txw%izS_7urCLQb@KS)}WiG*Whq ziS*cOwi)`}dCmV_&~GRt*23+d`qd=g)=`Ez?NfJn?b#hQI?l*ITMXA<(FhTo#)4=m z!4PJam+9dK*C0>TA4U!NruFaJc9yN=vIhE>3%`W$*%m~oQDb*scQg&rY+#2&g_qgT z(8xA^+RvE~?NOuA(ASxRS^rz2JtMr2iKVVxjf#8vsG9w=4`CP;04{U%%NlCwwNwQ0 z8M1HfoLmQIZV~;KY#)W0$W##ft&7sl5w%X^MxKowE+hGrOIdgF;En-Vo9(v^3BoSI z%UscGqTxhsy;`1ZL*ren?1+}&w}pj4)+5WDdaGsxqtOtskxf5qDHC#~%G2l7Gj~0Q zMp?%NAF0h|bf+~7C`B@M% z#}9w^Q71m>!*@S_`O}a3rM~zCjCKmTR7 z`akdF?@-wdR0B$7BrswbA?>N7>$|5U-dTM06tB*^zX|z9?kvKbB zmX%t|l9iOh-i#TWptq6k**I4@lB*EH;0Cu<%wKE{s~!HN)4MXoauP2w6`W$@TCRU=!U(0q3SB<+J5@JMAbgC1D~;an!9m zT|4e>m2Kn)xrQdhMIUw{-BwnJI_m-7U$>|Xj5hjv3K;nbOJHm#{U-a;CZf_1?WAkx>H|HHb24$#Lq@ki z=RD;)KHamBw^6`;R6)p^xBG*%bXiLQ=u&Bm>@8!Ts}Lp5v?Ul*L*uPT{ewDJA6nojAkzu zb|h4go*wpeaAZ$ZhwLiamkL5}tGB#E9XQkpkUc}Om4VQs&f3bBLSu}g8aCGr9Os*g z+^9ZmLyc`<{SMs#5ANfU=wo^hN}+4(LmB%^MzZRe@_c=ewp<|k+du+uVf<2o9+o9A zxKCUdZ>Je3#iYjohKo91GitmE)m%WJh!Df|xoTv0ROp2eYBI8CPL_@^E{HKx+c{4L zFQlJ$i{sb~FYdq!lmxcSh4=@&)Z3|pl>DPT|NLR!)V7q8zH8fizh$12G~#3~>~$#{ zVL;~#V5rx;>6BKoVB4Nv-J?Ec7k}_<&3J(>DnxMwU4L(Gb2CTk-?)s2P2PpRAInxbaLZh&>RZoELA zx)r>G?mP}ic;|UvjK)cuce$v~RfxhHV!v`U#4D0JNUJvBG*Aep&qEh|lDc&@B067? z5X3$l`~~eYdAJXC=(g^8kNYsl!Vk5J!HOA%(TLWvK0}UKZO+vPc(53=zRaLu%q@FF zTO93`J=IuU+d{ClFV%*M7=iC}jd+RTrplcUn;b@yVVAVn+o$*Q9(8KZ4u-)R&UYXA zv7xNd1gc|1B(JmClx!U1t^}BxHeG5c5 z&OiL2QCZqvxM0qXoFguY;EZdLLkqz z@Vb{`Y|+;xk+)fqZmx7iM z-7}E^ft~N#3lW!(`y>meC=d>JcF)y_>P&ofCMlt0b@i8Tv2Vb~ivXLr*8kl(FAHmj zOu(Fw2?aE%)F5NJN+YoLY_r$% z8I6YV$>%yxBe?Ck?X}G}r1*w5fN`M^T(M?YKME)^ zCH3F8D1TMMeEt z?C+%;(Q%!U3Z6CYqzkGQFM*_``NuJ!bF7uD1Fy1hn4!_Oy&=(1&}-s8UpHzlf;AUG zp*)@ko2VNUVqpeIVoyF|TlY`E$S-U;)TgFv*VkWTTRD%WZV=u)21Vnl%?Zd$P8ZB+ zy~2LLi0}~5A4jH(YlCI)z^tO-JU>OPt|Qj2X^8rfQAX>q^YmfULvT24fxvG#xfib; zzR8b0Wfszu_$K_U6bcXLZgzq1y@+aC+SbuVAj6#K4mO^v8r4+zI<)5l`snkhaX(Kt zHefvV5g4DLlUO(h!OyIA)0N{TR@g}edW5^D+36GU=CYb04oV%NZ4F(}IDHl5i(^vU zjR2m=U|04jRG1_@Lyj60PS=fBX{yKLnnbCJ9=V;RbpuQH>^j5o8p1D5itu`XQIZK() zk(-^N9$W*?96-G{kGlC2)q@~?rXT8S$9t2O5xNx3odg@mqTJn_nxP(4_qA9=jWNj$ ztH>!HZx@23H|GwB1ue;wo?v6RzDhfM`@le1KmB^NB!E~uGOdLQy zX>w&^X>w#PYIARHRa6ZC2L*%$XO@KpXP$!vXLWcB009I50000400000?7dx+WXF*m z_`YWQA7pq&2ExC>BWR|F<#0@+2T8Ltw0&7yqqV>m*-HYTHL+Tbt?9q-@wl1QRhWos z%m9~}<0XtV5>VM)d2e{QAOAY%|NCG6mw)%~zWDF&KmPf{ci(>Ti+}e;EtS6ba{kxv zK7RP!hxm{4L--H>?ZY46$7eo%_ucou`0A_Qe*f*|{fDp1|NX~z|FwMg@poT+_5XhN z@eg0&Kk-+8`}hCxZ@&76zx|v4@)c{Tqtvh7|LMcmzxn#(yYJtB{NuYny#MM~|M4Hc z`pmid?e`x)y#F&E=>I&P@A{bd(TCsSztWda|NixN-~8#jZ}A77efq_p zn4kLg-8b{~{HMSF55M~5yN@5=|M4H*fBknK@Q1#9@wxB+=}+@>_|Y%F`^yi0c)avq zUq5pF?!WlOuRp%~`Va3v;*b35-S_W)@MnJi;fG)BpMAOcxj%ik_*ndNvF62wk4DPo zo%qYehq@LTUCCP$bLHiSM(w5IXVQf6_t+0V@_+raKlH_KKYX+PS~yy>&P^&ea_?Qp zzFdAxt;Rou&yWn!^{0Nsclmzvf~b~8ZT<>VZ`D`7f_~iZc>mk=FQGE7|LCx)-SqvX zwjcQ@FaN{;`0`(E3(|U#TJa^iOVu`3r>wZuWG+6)s*L5g)Y^)hsE;~7^3!dBT4Qgf zD#Ca{_b~;L(rd#Zam82R;Xl72z+KLNgt~~g(eGmiS_Z4QtCGg?tqs8oepZQ`y4%tW zs2EHIj)e!0HgZ@&FN)_GgGk4yEGPH_j(ZsY7*(8xJIuiDVFq}Y%}ILOo$)jSW!S4T zJhly88bU2by}|@oI)XzALtuDq(fKQkt^3Ft7l;Oq7|y7*fj7QB9_%J8Kp}>Ky zFan@Kp=!OV9)D2|Lx8(fkK0euo2%X(%7zVJ!VV(z2mV ziRkCa2_6LpA~OlEEPXWXnl>Z2Xy<~%!-K-aZG!}HwD`6RTah*bFs?r2@j_a^{XIF7 z!&YQEfQsucDoQyHh7VNqb<)s#)awTUHH7tKsT4C1 zaU45xAywzre*R;ZYk`nzBjD-Q?o%NA`7(r*m0|z_;iwcNCzLdaA-R-DcWS%({dHt# zDfl^FZQc4loK7LQeM1^G6Q~?{A%G9h!0b`(?q|6DhroeR&&73x2kO+eKv0Gj=}b;= z&YQ}x7#;}6!5s%kHAz7b2pAw4zhNK(s*Yv9G=m{n^&)U^=EM3}7eZhF1<6szmC$YV zSgI6x)@;au1LKRd>#yJ*VJ>ZAXayk5hf)Xj+mwNiS}}m3+;GF&sTETzjIi)2IP|nN zLzC!Yzn=G97$||Pa&@DzH<+%aE19v2N|r>S;;zgz!K(^Vq{eB>hDZjPt|gvb)ldp< zgwoMk+~CD(#Yv;;POT#M%+f!yIazdK|L8IB;^;*bUj3Dsp$i*~rQric+ww6FFGR2< zyA`5y7y`hLh9O+x52u9*trcl-yn}p~AYBF*Mo!^4Uqmum7LeSzD06Fl*4v45M21ni z&54q6jd^(+30_*;+W=Q8)CK@kulvy9ZF-Dx`#`4fm9Vvp{F$kuLWmIR9|T=SeQanoK0u-<+r>buPn6zBCy72rX=Yr zYB8f}2%XIuS>8fIEgjkGD@h?W!ns^02sg6`IDUju8r+@gNRv4d0X_}<1{I)b)1KHN zNC6gR#ZB(f7+Pfm2>)Dl7;{Wx;l3|TL8!Faey(cLV;F*zI%Z_R(+>)#n75pSyzsdp ztbKxT#0KO8*Fn_v2pShsXPn$Ip`4|hj7ZXg#qba*+9I&NBD@PoMAdU)XVD+d#V*jM@Q0_WC$VFGoy~Acz)JivkW03 zLu|DVb)OUD%O-NPi-3>?#~_>0vkOwp0jZR4>vwW{m7s6Ix-$LA_K@WdwMl*6CDI8@agy7+bBw z=}tz#NEZnRuh+V*9!jreG&W#b9lO;wJVhA{h@jV=bdsh*6pWAw6JC&s6J^aDG%%jW z@851O&56U$Ors%di1BwsmfIRs5|?1G?pS9QvFMw%tabqO6AT{YD*HVa9Ry3LaFS#^ zr;wKg#}a@QVdFj@@N;~+9)|)FcJU-i5T-rixL;)VwT&pWM=E-!1FX9ylxSl|I z*RHm}1Q%>Lh=1!jBge8lva4g%prSFNg{AS9ptykhj&ORB-cL{FVMQ z?m}rpdsL3&eysflj0Mico)(^xFrt+}$HACNB2ijTTUusV!ZW7xO>tSu&F3#)M4K}_ z2fKWl+|kQu&%J@BrAO-U`NA=UMZ4)Ub}R7(pE&ToTn#u<3D%hF=;BY`WqXXV*E=7`wq*|Qk?|`RN>UM^J z!X3H`aBYPdvAgTsOJC9+`ee-33iP^D+JP|?!A&uzWAQ1)?U%WhIfnr%Y~Cm<#R0PW zphmN5`?8S+6Hs%wcAU)E%w7l$!017Ab0bNvzn2g=f*lx)k?aiBjE6`Qb<{F&nRV>N zlq7mAxafVn@*!AA>DoWBr{ypNEBMt@>&U}xTHe&agXYJr1?Y*&-ra%K%fN799>I$~8gsAFL0o#rm}sXP zB^u|MPxe3v25gMC#FEt?NG)W-m8%^~h|2+!Sa$+?;H$F4vM250fSmm{fnr%^kKP8G zI-8gNIa0Jp6ni})=EgP0-i4j9>a$QJVy~)Tb z{8i3WqKwptxA*2bD)u8@d z+pUj7fy7gDYYgg4oTF+KiU_f0#>ftiy~Aa52xm&bhVue!Qixdj4+g*tSUB=QuZZp9^_Mo9Ydbbk*Nw#xsYf=0 z96*EqFh_L=2TF5)onj_ptl2;R4X&af+LzBPqFkanjGG=w(6N26rcWQmG@owPCoVrkyP^7GK0gv_6b()B8vTz+n)L|Fm!}{q=!!%?`b9q`^o(H52Cc+1#c61 zZj-&pbai5%!t2Wi1BLkR#ziE~h(pU|NeJ z4;WXNziK4(Ifcwmgqb<~K`Z7bQ`|jBO)%s!pO#8C6GSaY^nv!SG?=RuB*9=lQ5>~lYHVrH>}pnxo}?XZJg=`0K5 z9o&J&uGMr3%j%E^fr$@m`%Sd&L1&~fjDG02Cb5IoM`7kCE#btR=%rSRrePm*m}H3$ zg55&Q>|jLS)Cq(8xFTJ=jjN~W65!=b9_+)&IWG@(j01Tv)X`N;c{fIY8xIG0g@Vh8 zQv4#LlMC6% zDTzf)i}YSuZ;NE>>T>A>rvOW64ue_l>kf@6w86m+H%CvBID}SkNg^~{>HhMnGUHK6 z!wQV)(CuGBMk)A73$^l{3>zS&&f?S+B&10qff)gPo(Ox=$|m?+BX~_cL%2lj2U;`^ z{(OEz9)aMvgT_L+!XZ{_4;>qx|Le1#JvWaRrt!tM@4k7DZ}r80`G3BD|M5Tl{eSq? zFW-Iq`2LUo@c!$+`|$mrzj*O2zWDy1{xm;~AN}&Xzx?2b-+lb>yAR*~`udUgzx_tv zfB5{#R|BOEf%8HoA&IA_YhVZ0mGdvyvSm*Typ)Zfr@D;Z=<3g3sq>w2Q zf?lg&Cql)?;PCu8k)oA~tm7&=o}`rEaD;L$D@RPI99c(A%w(P=IFfDTSm7Wu=nbPUKvmlwL&nH-uhK_Hc1PCRG$%hrzLQW zi!PrXAqbx#fdALfv0mMc=TR|b{W(N`9>We~XfK~YPgZe<7P|SLX%RK#iKNF2?N;Lh zp`{C51tgQqA%@yjnac#5t@SL!1r|))PJ61xIdG((pDjRM4fyCcOUYpcrHYKH9^MCu zoilL6S72bG_$73gR6)A_h^ev`{5>&-xGyaUQpVmChR|r13v(Dk4VA-5@jtr6l73c~ z@S~D|ACpdK_Y!ODpH^|Q1cqLO1+lx;bEb{%Qa=1SM^6dpl1!?%*@#RM3g|`4$lCuL zEcyIkNk&VcK7HT#B&D1c6#SfJBN;_tCPDgPY5K^)PU~?}=Ij=fEqR>+1@nM(Z(HJi zSOP_R$w=@uHYq^TIDjTwBGDzk&S+ovBTD-WG^Sf3aF6lr!n%S z9jN|@&I3iANsW82nZP3KpqIU5`oF!JfmlHvBzk4%Tc5b;=P@BQ<^nfT)ic$K;>5kY z#*Ck#r+hIyqzrWe2F#`pwd$>VmOo-mRYPJPCju~a6j!nsg2#dqlE>E6tmaruQ>mX_II$5jsnlZ9 zYCnV&%bN|R;ebyFojDpvwYc{qJ>UjFEICeH8h^Gq+$0!H_-SHn-7=x68H-aNiCAlD zna%aN9>Pv}n*=RZ3KUtup$xyYZmsfJ4>1O8g@~S%a&jaCR)XRj|9GgwbCO-;7Kat= z^hfq*yXN0#XUZs);@}pm`RhL$1(K-_ zAZnrRlGW|w;etJ+-vry%2j4zBOGu5767&ueI#x209DcAh)SG%q+RUFV_NgdDPrK-7 z^snWl7(tp{iEOl%XW7WgIH*XF%;1-JR$e0HH2`m~2|S8^K1uZ2N{6qg9hOwH@Kb0} z#ON6BryKPDtZR4Hz9)0Xut(}VRW3?wJLZ;}5g?blBuE%48%U?91HQxT+^o=Nzi z7M;YB+`Psx+<4P1LPC--Cj2)<$g$Q;SEXX&l!vPj32&cDD2kl71`k`>{vdlTB%i4T zwXS&SLPoE|gy62LhfqqINir%P6;${@8bRIeIi^T|+}+2Aq#Eo3AFsQeySV%}8KfkI z;RZPPJO)sX!f+dfMM>&*!xV7tvteK63|ZvN)-2dZJj+dQE>p_Df`hT8g?=I8PfwI` z8~Decof*jq{~T9;Zj!;iRi$Um0FVGUHDl&iSZID!lrV0{DQj=J1a1l+Egba*3&(9q zvOdxXHyh?%54^rSQmNzf8i*7t=#4ZPA6>)&iAkUvj?L--JeFqJpVqxrvjNsV9|jOg z%5eMzseu0l&{yAtrp=ELJpHU}YN%EeJcB*aALnoeR=T~3F=6hXS3eFbh-^sPm4$X~ z2r+v`)$%lqwYbv!1qS1;c~y-BuKcrOLCr$zz+UwRT(#Ii=bDI^Fmi)%1|eh9kwT@d z4CBx~+Yk=H7kUCquDW5tVy7OOo&)J+(B=e)fZ5eGTXsV%BBC>0g{G z#uIa-T$`OUEbWhE;@XL1}KfeFtuRp%~`Va3v{=@sP|L(*0fBxddxA@}wfBMt>Fn;vQ z@BZ?GAAa}o!|y(P`|ImR-v9O+egEO>U;N_w7vhin>fQJ6e(-00AHU(`Xv20qxYd#D~8Kt$M3lduk zaOrcGJoNTOnB7iUmU##T&uI_dJ{3E5_Rt%Ah}NSZVmo`7g@MUWPKo@31b#b}I_E+3 z7y#&shKz#H^$gbs9co3BI%b>lN_5d|}`5(FgEi5;YiD_dyeN4<@I8|SZO9J90zydXnQi4A-kp&C7O#qR?Y zS}XAN@;Z-V5Uv0t)V}BE0pO4s?Ap-IIF>xV0B?xL9IHnDjmz@$+XCMvx1%$G-$iyG zpa6e9fQ5{&)M-m&(9)OzCs>aH!m7r=wbf=uvf^#3)XC)P@4<4-5eO z$TW6(xEDM_nffPs9Yo1fXNm@EEN4Y!1zGYr`xp<=lbFGR5hz@H-O({cwCGaJg63^j z6xVE@?Qjp{r?2KbR;)m3+2*buQOGgXQ_SI7S>O@P;->ep29pAVd^SV9v<5^rnd~*! zn&R3TJW+As(h(r}+zjGDOkmPCbR8O&u4}v?p&}l{4DI0TUQ2(9mi~OjHIG98T#!lP z;CNG2gp&?nPHTYE7hsm{nsQhJJia1Z7&L_+wqp)Ct%1roo8FOc9cyWL4uF&Ufdva< zM%tGb;3`Kb{ot$*vaYo%qz5l>JP`L5$^eN5TI`^E54LwR;Zr!hZnP1s8l!J_0*|eS z@SWeZdUh(Ig}qS^nj@Kmy-R~=S~SpObCTYiGr>orC_XjV1N`JEOB>g=6m^MyPrqNV zXgIrLEOiSVG$y^C#^RQsp|sO(NGprN99yf`QMTQDQxmPX zaqi{)h>^JQeC|0&fG=avO0PQG4?Hw5nKpvD&0kKaz{(bK*0-<26X{!SJwN z@l)W7lue2Y?!rrcuu~e@*Ft2E&FgA>zpj-amA*|quV>|{Af|cniUU9ZM=bjkUCNkh z%-0#}JTp%4pxyr}I+FKoT1Bitr0VUqg2ICIB2&w1)N>{1AICRnH=skpS-*xO5h)@Y z-4X~)92xP|e~$I=$3hNPcq$I}XwVEkWmH}d6dpYh_~`qUBpDbIS&b^`ZilA$@Tj7j zRNzTp>(iF5&!zhl<(TBNwRD?|J_AJB)i=}zn`>fsD@elmGqS>iDQf8&w8#*S8NEYn zemxN*K{uRn)`d8P_Gh{9AX&^?kPP7Pxz#zc02fRx_OyrAb$!-Dmn6XLi(PXYL1!*wTaUqFNz}?b zy;<=fuECM&sx;%*8WmA+ezsDx;v(rk@C%9hR&bhu^(*|Mtp|jEGKL(7DL4aw1IEey z`c~E2P&*EK*S2!F4GVG_AGBG#9XU%+@ZU7+(mh1mM9)YquiG@SPs5Dl?rsqRcyHh= zX9KC4wDTD9HlD_n#~A_`TG zJ__E2t|{zb*|V;C>sKWzJCPKk7kN_J#mm{E2-OZiL+%@<+R4k(*}V_h5}1)nqDko* z%5I0_W;L$e&&VJ8wSSGwi^=sqeTg_*WX-@s(RhJN8$7snW!!FrcK6uJc4qp$viprCE)@PVH6aD?yV#B+$~*7&aOl&Hq~xD zBoIZLXDvKVQ;1#TMuT)yc|?AogKNBwBZjQkXwq7P0TT1H27%iPmXb4E!;|Qd0*=_7 za!L(}TbI{-gW?-7#rTqvSxhvn3y&l3P7O|If_x@BWu@jZi~(?^xU-LT6quK+twH3_ z8ufv%NgMi}iyTZ&fI&)tqC}}yE`|O|;x~8R&yO7nCmH<}Z5siq9`OGL*U)6N^;#z2 zXihqx3sNn&bBAId#2PFL(b2DDj zTX`ZS*(u+}a&;4F1Gi#s+kd$n9;R!Xtts9b@{UY{(SMaCy9bzll*#7fdfRnaE8r14 zq|$86Y-*Dlv(s4 ztw^f_7c0P26zlnnuJyG&1xJjlY4;%s77daB?ybMlxMqt9282#G+bVEhcqS#Eni6@2 zA1U~@PodQmd=4fEDISh0M3>ij;&|nZV5rievmjbu{Jq{ zpy66uh~OHELv|;Z5X64&dv8Ex05Ux(@~;wGl0oKRytC3u1Bq8m}HPv zOWbGm7F@vVGJ{Bg#jwv^xlM~3UM4N4{3w-PNU)`4ZVBkXt+t@+&`q_O1$gr&!MXMg zy=5&ia50+%UvPSaOL0n8ajiw;e+*egy_YG}Vo$7>bzNh?XKGa6I+BnG6>EL|tOciW zzJ8(IQcUH64BhljwFWKEz|p?^80?|&9u&^O*UPqAUgGXVBX2yWnOpCXTTt3*Sg{_u z^y8IXTNa;;@pGAMJ2UP5SOiM9haY=0cCvio!x#fjn(+chr#}1P zcxhcI`u>S(kzLPLEPD*z;Am8lyBYD+qT^kp@M=Dc_oZnf$r>qwq=fvLJtQ^jytaZ`{1bRBfg~$Z=qXkWq+zn!HOi zq3_IHnuS;vEOVJt#I$74IoW>y`oeV-OPNw|(Z7NA*=s1T_)7TL|1v>s!zeF$(x z=n-Tx2f&3H)t!#v;4}IZhXbM*Kkm)+K4cw;ia0#j1CH9)#8u%ODD-If5T)6u(nwaR zf#B+>em14k12DH}kvZ||B2)I1cE(`B{TP5)a|067OA<)RG$q}ADxO+EWhKo^+#<10 z)3}F{scfViC#^8oc1tnW_CR`U;O3Jlx(VjT65_zIoip9=5IZ%7Q^>`AC_85_v1S#qOp5&|V@Y{5&^>4VUz9RogD=iWMS0*6+< zk~dhT?rfp8r+J1=N_i|2Usu=j1c{D>a?ZU?9Hm{;O3*~x8PLFx26`Rv4898zRQXU9 zSlWWgi^^l7vA0@=9?jU;8LMh(l`RpdV%;G~njD~z#;){8ktq5BpVlnG#$`^2;g$K6 zvdL6kP}NUlG<1SF_^mGy^My3@E={V~Hg^vWXlgr-^l1#OHqr|Gv_=dUR$QT+0?2g! z3ie}kC3=A!bUdZ@PQeYngACnXou>C_YNi}yuyQO-Ad}K=Hd<1*5Cye{oK`&^H#Vj= zrl3Qw8i(rv>M_#JG1THr$D}WyRpZ8KS7^v%(>RnU8?HR9HMpi834B){wnvauy;!u| zp7=Zxr4!ap=KOn*k+)G89s$`ioV@Ku-eQzNkzjY(@_=2)`hkh_2@xTdc0EG4?3|*`et}}^n3`DQ7e-VI3R`}S;IN;-X z$}ja;dMI6N5_487N3g3x&2b>kJP<2g!J$ zF~!4A`&64yVN&Oc%`T_tX<5FA3rR$EYeuX_oL5_*;0SS@n+$`0+SVZBcJOPoPDmuO ziV`7T>JnwA+UEe!In{J2(u zvZvM?t}E<>1dn62?LN4n0yt2MTC}t%vn?mz)XRt|NQrsL4SV##1eTH?G>&>qP(-pC+bfYsp_?i3>oXu#n5yN*qVHbO*0Dgjuy=lE(G zJNus&uqo9hu*KuA6Ix)b&M$>!kKovHxoqv^di>>LIe zv+zIabb>)sj0a#=4o%&syuY!x7-Yd;5thu z|JOaCG-ZBHAEDjoIpl%ZF+<9x-8=U(j=vj041f$a-5WgFWm8fLqr10SqOX?9Oe6kU6TF4OcMBVih zW#6ui4InMm+qUWi=aE+xoh|E<HdJ*QVr!v$8&pIS72}1jC~MNQo*6^#S)ZDHXPqEmg);ZQDXkPo#z( zbZ2wk@oBI(tp!M^m`jyLxX4@K!WJ`4P1d?yl6vcQD|0B|j+Y#I^IIt;swTmC^`D$_W0JBABt z@Sl-*UcSFD{bp*2h^}io?5H`J;O7;WV>jeLK9Z_Skj7@f*>#48VDDim1jIsGEe18T z@5r2(5HbvK($`fdSj@{-UM6b*R)~{wpzXG<#U$Z!3O`6ea|w$WJKqrMo=+v~b*SiC zo&yIOnYt4+N8^!B?Fw@_icSMz0>_8j9J_;Sna0_ws>5HCMh;ppZqFIEGZ{n3u+-}0 z)TH*jD|NAG4!9s8BM=fpeX~TRUKLqK=ogU`Q+M5fW>(B+^p0=h!(HZ=~-V^Av0}DG+c$ax*BrF+Pj<~Ibv7&aM2#K z$afodi(+dJjz_pD-%Uh6Kj<}AFqNtINci8go0X_-sWs3s0a)>%%!}6^9OB^~4nCGO zgbbO*SQFUrwZsT)&hA7EJ=PZr8fT0;T@k}*FgO#xerY#(kDNSXnofZ+?)+g1G zl~>>^=XyG0z+D+LO;Rys?KN*Nn;kC{*-Q;mP()gKZ}?;iEH^n1Xpnw4f_gtJUv-ad{qU2}LN7n$7T1?~+nbq)yhWFmFUHHQKqs zSE5@X0^_tBw**l1GGj`+PlLPy*hNB~|5`KJrWe4xSYReJ*pT+c}d9cYXJR+bT5Z%YK}S!>dv=|>L*j6@VSot zL_2VX_Q+LejRNFJr$JCJ@=}m@EvN z-OH#>s$Q2P)&=RDVd7HDIA(r_gtCH?H(RI={?b?<^gxs>ZM}vCF?i9QJN!h-FrRIfBW8w{cDURQ60Fbdo(Kdt)U4Wo~ zAU(PdOC_3~xIsZ9Fy3pGw-nAHpr3aOs+LY$BETq`%7F(qO>B9YTUw@JNMzlU3KzGx zb#9q?-+&gPL5s8^n$%m~H*o=^R;1hOK%sibqbH;fZl&|lL1>}>StYZ=fCW+Pb-ZOK zFrehb!}e-5WHl*~%U`GN^a!A#4Y&rWI6|FL`L?M$A)mNr9GjqNR$fxyBr=97M_GvQ zY_2j+Sg>kW*TBGBY$SIXaS8kwJ1`+srDAKzota)LTt9%OZm6Wk%k+{mC6{ArvDJ8V8e;Ra_5^?{f9QHmVS*Vp<;rL&c z6#eE_sxCmz+zxCP0vzKUO2~(R3|bHgS)5g3XkIsA0&&b7w_`gYt>db`c9Sc~V+}F3 zGgpgMTc5m`Nr^jvAEQYxu7^r(-{42Y2|Ad^cdX(s6rBZ!)6iI;``3(IDzJW(+ zJ(8K}%Pn%J%bGVU#Jld6{~xq?V=l_S~h(eSRo(9^0;^uqpPnK-Ex#EdK3-0j=) zLQnbY5lg4ERVK>I{TIZyGyjhVGwJ4O=UJ@a9ePvuPDm@alujSMq>SVO>K1{rmThNkTjga?-kG0L!v1ybVTlePyvJF ziIA7bQ96~|PTgjACNHTxPFdu|qa}3>+z=5XNet@0PHPoUl{(k2yB^)jWxFnh7?KhO zYtgxGJ8)%!KszsC@LY$3DTHe{S`Hf`acR=kIvee1OW^5}ZeMe7f<|uHEw)QKpHYjJ z!y1gL1fai%HH?rkHD)4jfRehS5?5fSD=qX!VPIrkmZ-&#e8Le)=hpFKyN2ig`s`=V zjpT)aeDUqOZ{Fi;eesVUe);a>$M=8yKi+@*{{3J6;r-Wt_u>0LfAQi=eDVE1{b_y{ zKl5c9-30jGCkdZus|cH#Rx;xd>>PIjP52@)dC(OICEJ1D}A#m z*V*ALLXK^w2kW*w-FlM{U4h!Hg{HC^C_Adz522=Fev zYfUt<9`v2;GS&=C667OseS)hd*sL<4hTsmELU;~@Roe+^#T4A5dMz|e6$ZXj@4Yic zpn~!1W@fn|3N#bCiS8}ICeqaxikywbM(kFJJ$0UW7<|97nm z5JRso*c~&$NO;6~aeW9?CGt%Ga3mAd$2cOu%6k+#XauSbWMPShH>I#l{1{!^m|xAxJT$y? zk0R@_3AW^FZIh9!4}9>T{$9a~c4hj+-y@TssAUQh)J_f)?dBRZmQjb_sn%dP#KBIr zdz(w3s|sU~<#THWWFx^@wK4}k*o{BQ6lR)&e8F4EEp5ljN(fWK{fXc?m(;xy1Dm15 zX|)|m<8HcTW?*nLGWjVom7Z$7kv{Z@82y}{_G4&r;_^rNkShRS#`&bLZrT(}|J?lT|l z!7?Ykvm5R53Zx8-GY;W**u?NPu>irUW}}ap^z6(pL$r)BYwUmOA1;MnlCiM&)6L386thFD2VAZ~a zy@Y}{bE{LLb;(}ErKRm&#7S*`K@vbhoG{?36-*P7RE4x9Sz0SNc@yxhHE|v@m^iHs` zZ!F+i{_sWyZ_l*msj68=7^3gSlUc+M)`C-(CXPY3)>wOJLf@&T&;j#guv)7d6|IKP zAiK1NtN~wj%~GX#X|7{sq}$0^r@k7F$^Jy8TK+Mnse|mC|u`i z5OU*Q>6~55N7%XkQOgjQAF)-zly>a#;-xb`xAUxw=Wy>H2?J8ugLLQ1bwAUk;+6YV zNk!M%7)e(x!x+Zd^mGr=r9`wx<}xaP-_br;Kj{cr?>2DK@DNLr@j2^|k)19UMrMa) z2!h0J3izj@hAWp-(7_l$_Bt{-?ovS1Q3pk>4$x9z* zipBBEgOsH0#1LYLMW_V_w#CHXdIUB!d2>>php6cD285mh7Osnxz^0?aHQa~zQGxhI z?06o#L2HsvvQis&poy5_k1pl%eOy(oZ@i*u%%K+7vzKk<+RY<&WcPq%cd0Sp1dUrm z7J+E7n^uZ-eE^0Cn^PTPi7$c;`h_r|CaOlqlEloCj+xI>YECz>Ivam>pP8nR<|Rk! zJc8%5E=dOj-@puhVGy%yR_bDOA?tc2!RyU}ngB^lFx{Qu9j-K>@IXMS#`l?UlYoff z-fwGCs|h!Y4?PXPuBn7!Dn)$qg9odJ56@I+80YGC-w1C2yKtRE??f3rLi@sknYR1b zx1l<6iDM4f)~m-CEp$}yVOc=mBhy?{(YaPPG-UToDKGh7E3p&<)FAP!>sm7fH|*qX ziS_JVWujTyqQfXmGUrSHh}sTECt!v}#sHtt2vH#R0P2z|9YB(UaUS1MLZfln-yHoI zfW?cOqD!7?aV8%8O#X1W;>aGGy##N4mzo2Pu(ujvV)Pmup7=r)~5h)Helr@`2 z^}-P@@HYk{RwCZ$xpn-!FMGzO#I*sxfOsdiZ_O-QI$OGmPRH@?weL`so$=ax(V!F+F0 zdN20S7YwE*z?aQHGzW1BGVV~^XgtF=TxV$<*RsPZ?lb9vgH|M!Eu!MCpoAwydM(}IGIhh-Koz(lsf2ed z)4UK*NITm=TtZvdLb(p-sLL9HwxaPP!>f>u(D&pUf8Nx|V-2xVKHEvuYM|+~4GP#j z<4u~7&uPbHs1W3qosc?u@8-&DPdN(b#0|K&KDWj?xkgOwS%;(}sc3_dHx8f%Jg}@g zlR?~L@L)Z5u*UL3iI=aV2&vDxkS!@K0|kLKS!@%IfG};{sE$>c9`Yl>=(Jfv?Zt8q zMb;vnjuADoA?jxcRoQJ3n__|x4&pA>0Gkx)y)sb4qy=5}tE3=LU`H;@(T7@r&8}IS zRlp4=kGa^bb*b<{&PMlv4YidXqwWA;K%c*GfwC*OMhs{glr6UFTk0r3*gr}%4+-Qv zs||ew)s^(Dn-nqnsyfVK8akL8*9qnvsqT%U!#QG)e>vBXHKRbP#CER06se(wto@8(>2W0!k7`a_r+$zAWeE0-4)7>Q0^E3+J1S6= z!)B#X+fm56W?jif2o$U54m>?Zc1eXCHoy%BSjlZOD$PhIq_wT=p?Pth2^e1|Y5nL{f)R&8E3(oyZvpi2MW3kLQgo);@4xt+K zuj|RS7?ZB%h!nAd85N0Ta!FZ*kfj87Zdil#HY%N?X7~WomaQ=1L9YwP%aMhM5-moV znr;Q`>?+Op#Ga~|rkF_QX&cxQyK}JgkRl=)v{}X$7Yn9&AFvqSkbEWG#jZg32LqFO>*CasPC?=?_iJ9u zfGn*p8zlhFUTG{p3-aG@b(BYfi4Z`vGxr$aBOX-Y)aO6t{2M9Sp!4J zki`PX6F1YcEvZpNuo!7OMy2~&g~ngV;7g)8M{N@UMQvnPY01LF03f6-7<*dpV_j2< zJ`^Kz_NfQI7D&Gk!-3>f7>U-)9NKFd0HUW|kht)KNYVrP!vnNs8G&Pd9|;EXn>MDT z?YbGoIco|WYo)W64m_lTThHnpVk%7{Wnu-KwZ0p7*BEMuof+Nj+l?5mPnc17NdC^! z4{iZKk!KYYK(Y|p#()j6E4J39gYN*uY-Z+jx@~zcqPmcCb840s&Q=t#8;W#ei?rfS z6fMzPrv^7?(09i|M(B@zy7-q-~aiG7vJKG@Bisf^TYVjFTeZC z4}SRF#}B{z@a?a!A9?@VZ}k0#uYd82>tBdJ@~d~>zx%B#pH;Le2|`AbzHqi(UMwX}y& zUUY%qCF0HcUtViyg(jc-Fr@u`$Tp4`m*H!T*cpQ^E5nZ!O z@#_Zo7CX?DOsF#V-gi6_X`RDG3eByrScnC*KZlO&F_^Bk6({E&bCsKAPc6@+b9S3y zX;Gw>Hs)PGtmwR^`^~{35w<`Lxv?|HaMrSMV)(M!V^+v%ie|m7NYTnT88Fo!xeUA9;btav55 zZPd4s)h*zlJ*HJ^kUQ+&Q4ZDsC5o==E4)+4II4fHvjA9;)EH{?p01q3qqV9dt^DGJ zrN!)~2r06r(U(FgOe1vM4L+nza+$YPc|tZ(=cX&JtXCvdl-4%@3X~YMka4Xw?8I4P zmurrW5N%qodHqA|ue@N~OGoqpH88_b;S2ekWx@08G4ED_6QBlh<=}O-VZ@qPsQ*VjVpFV4n^kZzwHGtXXE=xJ z0vL~uu-)!6l3^AWDN+6!%F;JQr7}1cI}A~+tx<#Ktv1xD%L-5x^v=C!}=c5la1m={ByKI8o;&FoOw3{3yh+NjHQpv<7b z%7Lmz>`GIomUhOwdl2`M=#8gv6Da_71vj5$M|_^xHK^&iW4(`&UXs`HSq}UQ^&l{H zwO2^+^TvrwU?!exe0coEmC`N)O>+QVzKSYt0!5fZh$z)JS$c|??kCUuM{)_QdX%T( zg`;!Me|_)JxBxn^di?sGgg3Y8EQ5&*3Gs%sF>USX(hI9j_9=xI*E5oeN2x@Xs_+Vc z%vXZrFL(7yq`(TJVbXOoBe8S9Z<*{vy$ugFIzP!dG@U*uQp{?+97sHS10+f-;_Zog zbWL1rg*}!8W2t&$4teJraMB2(;T;GGCUUR51Jh!ogjNa>&lGcAa)@o8C{mHVwsfIs zi;*z^66SyekZ^}B@l2$LKsSHNEOuj&2qzw7bhaCEXnE_A-p}41eGG}#P+#o`R(+O? z)}cZKw}X&q9X+dcr!#T=NeYeU#GI739LZh=V!i=PZcq9mt%O!Dj3(BN*VB`Ufn@mc zlhwTCCX>My^2x?`vW5^yV`?2yAc&6FN^8+%!$FI<S~YS^ZG>)kMy8%uhml6n=x2BM`Jg1*%(4iero&cRh={hO zNNL^$8!tkWla2BG!>gwa6i6_w$KFVTO>Tu4xfpwoE+@^%+1^s)3cdkBG`96Uwj@W_ zv{TbE7fu65UXzM-wt%;epNR&w-!f1n?o)WI-aS_iJA|iwS5^R0z7!hrZ;C!XJ5#7rhBD929=~UK=c&>Sg;ASR4 zk;)uvHL@9&jxuR~ZQCMkCSeGV{TbDjo`Dvw0b+zSwu#@xEqH)rxPx)!WLVyKDdQEs zhRp5j8d@@+gaosacaY$&AL8uWT(XwPd283J%*I;+3BAY!jDYoR?h53sGBFjAUR9WQ za#t^@8X6*q;G}u0xR;tTcFZk1xNR{Cdfl|<5cvZiW3IBDcS8zkGPDEXHhN2LA>zYC zjIOaB-RO)7hAUvKjo_JEyU%1a(pj^Lx2bzUj39lJ!2uNZlK*KDV-(lI7J853i~C55 z7@HJ!K*rZc-I|pKnBZ59mE|OT;$owOHr-roQK@Uep%m*>f`ry7#8K+#()+sbEVvu+ z8!^=5wo&R!>la`PgC|o*Z{izXs|*z;lHjE5wC_i81&(gDb!W3~W6d9(nBprG2xqOa z)=oGmN?Uhsy;!KI4O80DiC-LU5?;e!Q=S-k@j~ZWTZp|!qjOZY9)Yz*tT;R$kfR}& z>2;$o;xTX|)!-ZISa+>b#|R3%;FWc9m1H%BkY^Wd#~(D0^g zflBq$9m8WR)KO`O4=ljQ3xhE9bE z{!e%x0f$-MmtF0Hp&3<7rS-8pZwy5V~Wf`IEWyu1-7suYd`*{{VM z+?$FM5znupnN_uU&$?RKtsq;%ZAXOYJAT7qG1H?Ilk+=iqQvc~7IXZ&!dk&i=KiX3;lNkW zj&R9`f?0c86dcH)DC$gjxT@YQ?{b8~0}5UxTp!GSL)d4vz*dm>`o0*Sx6brekS%lW-oN7^16i%h`@>n72M8i zYe14)@V;B47_@nFY&^ls)T8>`48O@)+ZjV`b}K84SUkDQP6U^^D*CyB|2~IT@X0#b zh5$NqRY58pf^lrtC~qgCjaZy!R_#;Bb}Yf;EU{GP-a9Zudt)PxEP)=y>abv@NLyAQ zDvms&YL68e7{xM?v=+rtB$72aphe!8s7#PKwt#nViq=deYgJ){TyeiH$86}IZ{q9~ zNIK@U+na(6UZ$Q*C1VSMTgpreKa3CxZ_LK)JxcWw?_qd9=APcvQUt7lJ{Ki(l$%;H z4bMkR@J~x(=UAMW%6y2Icr9;h_?e^fJkx*S=t=afegVH4kE+gafzs=8CV_x3tCg321XHu^Ed0pJ^BRLb_=g7Y zf|#eBT?l4pI0fg{>u#nAme3pCc9&O3K2sAwpFQu`uBk-GfU=R?2QdsP)QFHZ@m#N7 zGqThWEyTO%xEVW=d`YZ8XLX_m1c+$ia9SFU00glzJy~HktJAS!7Xo$dY4uj#R+yMm z8?^>_#uG}`Fd^^^gd5Klra+r&k^w_SG1^`kkIk$<7mcbB0Iv&DYd)oa{TMYTIHdwh zXvZwyPR+ha8Ndwi4TihH_QtpuqNR+3szz?!Debll4lqdYh5Z#e=pb@~tMnGSIy;fN zycCbIWmqRE%YM7;CzG;VRg4jhM@O8ndkN6>ACX)Ry!P6WRsuNmw5yhP;CE`3LF+e4R30WK8q$=1u*X)BPS;yN=+<%1J7X)T&5 zI#7cx2Hv@@$K5VuRvG~|h5yoxXlq^;H)K+?E(hG8b1pFLoD&_Ke$|i+%u`&amK+Ct z1CKyZ{I*uA|o`Q*qKeNts(e=@hv-?$Ji)SCN4Wk2@<#9a;pl& zAZd$`93u84xcFV$*yzj6Dr8ihw6ShINa7YYPG}yK%^(Ya4*2hN2fNWt1$mvs#( zwkh!yVFyk2f~|R%CFuNRiO1+jh!hIl$*$7am&(NeONBz0QFIFwat}nC&=fRB6h$&& zS;yS~2-P$iPDH`o)YrXAd`sAYvwIhQTa4FM1C!BI4lRvo*9NU1laS9(q@I+-5K zu(Dgqz9c8_nnj@KL@+$V$XaG%F4O8?8{9z-8BWrrbEmh(3hogbPcAUg%`51D-*LN- zI3;^8H29MU-SWy;fCb_zA&j-)Pl z(JWDVjh7=cchhsFm6|oDoa1fq{_V9K!HNxh!)}NUZ%w8&WX+#xh-g|$4$>Zzj=g(D z8nD%@tzKGfm8}iK*Ivr;B_;tbT3=8%okH$+w5nU(V?#i6V=k_4w=RgaB$Q^e692XcfLjUKy}*rtb;v8zGW8f2 zsVzJpcWui|v5$ho6|<^jb*<2unc0uV3=f9iAWbBzazra3Pg<)s>1{1OY9k0QNn$fYg8)*gMb&XV!jV|c95~6tE%aA-(|_`| z)HRumMFVMUciNW1t&c{8lO2l@%XdEXbNuLejp5SZ7X9pD*GE{m7X>PW&5wzWc^M=C zX-nvA59a|#HtL&_7rM}Ogk`7!R$0qS(E}eST^vd6*oxZ=z$5c~-TPuxV)R%`pM{_k zb;lk=yI85j$Xe){BUr8HS($@I+~B+pS@XBlins)NoQ?re0xq&ua8f+UUloZryk}4A z8KTo*#w@kVwjV*UlFy+)G>3ZhjY*Y8J#L&^WRNTUMZCd*^z}}8<>=>{%7 z*JjG2y)E<@a*Q;6c$dL)&c@UuY{^7-zTu!J<4C3De48Mtj6fC)HA-t1x4fo+7)pAs z$o+$iFVs<_Da>B?RPo$nSER*+TLck)TM?oanYvRQHwFJcJ!QV_D$Kw^dN7*aNsSIi z2)H7x_wANspSqM)=}O}Ew)}z>lIO)HPOBk-Jh8A?5MNIs_@!U)__DxF&1Mn$jX4Gl zV>qbd_e;eYS`zFr>K+eD0L!@DOWrPokD^g(A>V4@YhErtV8$sYVzoKR2`Q!B(g=;W z90U*d(WwkQ5p}YTP^G>mqyoM?aBe4U!`a{1K4mm(KQG#sfRLrOLN?~I{eI-J#k4fA z0=xpfK?$T@J=4I81ff`;3B$}AV zEw%=p3Nhy_RUX(en37`F_D_K=d-@4`~knjo{Qa|GT3DGI&6(3?QW`Ly?E6c)7W#Rd_%M0MZNV= zUt@#LOU)7z=hH7lxyih7QnUcC5c2IqgQ{;coB(`KI0n3sV(3OrCa)y~_wh6Y($SS9 zZTVTXbeu0-f3I8N#n6qY_=b4f-F8I5nWtH}@*FQ9!jp}W)f%Kyagq9(bi{>hl;&e9 zXsB&^nJqD3sHcxSK8CuvmKNp4UNy?kOQui62;e6MY&?ECL7JU) zcPcntyBMp)7}r2Xu{>(h0v~CBxyHL(BCkC(B;Ga&yr?zg-JZ4BmxATL~`e@=`k!}Pl=9}0chP0;8OPfszr2nyO&S_<* zF2NTs9!Javzw0t*9Ie1$fHC=C6Zf~(x*nw&&vHQr8T^*6$zdy@LhOvXlIQO#wApWS$MLqtV_&Pq+O{3Vknx_-&QK2(-ayq3F@cR zp&w&QVrZ$rL-i1#^kt4PY7hVp*u!WW#k$1tVU(cah~ytvW{jS9zt(Yr9{YkK);1gq zlk^iVfgDb9rzbHIS01U&#rQ_u0$rzFm{Elb^bnIkgb1eaWgildf~Yoni=Kgt9>6hh zqiXY0v_%fFA9C9^EAtwIg*4;rD#z&K8H0~p z>eVj*n#Nncizxs*>dp7C+IV05ib;n@-`~X$|ta}ortE67a~E0 zizqT0zp1h#BjtX+H6tx6@u$W9RGoS{wRKsyqyeUN%6s2ng}=3}nSL_aquio|_1c^( z-OCtys0SEcX6)0^#ugZ#aD8PFw`E?Huy%Ia`T&m{n;7^lcXA9A1y&ot${hLP&M$C) zOK0h1YyG9Xw$O|AS!_chCAKcerLYB^!lX730-Sflt$wgDSnB|q8qY=EzDLS_-tKn`qY-ZqsG?(?# zb!N|Skj9@VEh5*f5)PVrk6O-sb}NGcRtbr13%cLF<^V^!LH7FKU)mBw@ao$$h89XX z{~Fj)xsK*EtBUIz&!u${oG?<3&0PE?v$TjfpDl3x5F?MIQ3LH25H`wc>yr-15R)u$ z2~#xfZLLX=7n^A%eu3@SvvXKO^U}z}45Djl%Mmsz)yxR9jw|r7P8glA! zk}W(08w0Mha|;PuX?0@@1VyoLlJlx}QB6{|LE+D$;~HM0rFzQr7*c z5cd5vI=^gg=XDGhyrN?=J}Qruv{~LTp{1FJ;JI%XI(0GM6B0xPC`O+_2tqSd0b}gNtpOc6qlekz7i& z6bArgs5x@lWm`l5MlLa`j@h+bCzp&cA>J4nEiUm=dKbf-LK8QNoTWx*hXq(nZpe_6XpSRc&nmLawQ*xQ;TUJMWb((-xzVNlXF! z|G&L=*_9+Y(u4N`^b%Z8I^6E&vm8i*b^-*xK!eke2x&M2{#-~4y}QiZ<5Zub-1F3k z6&d+^2n`Z!*6GfZ;U0GVE@``ARp$lv9?Y3^Sc3t(qqr%PAy3SyVeSrbHy}0j|8AiXbOpr zwmQP3y0|QfGyB3|YoVk2kC@32Phr;qgJOZM*wdw-=cPcllnqq7I?GDD!}nDTFHtcL zJ96&K^HT;4hvj)ow-<81NWRG<{%fRwj^9~KSzV%b1SG|1IK27IY_9EmH-WckB*+R_ zM5fxJc&u|$%wXCoH1))A&t0x}HW`ujy)N*sLM>4ZB!#pIto4sE8*E84S4_0{y%c-W zS3=Im$iD@y1rw1Ao(p4VsGe`J1=$VRBk$cH~qJAzCU{}8(i|#@7(@Q?*TIchQh}fd5 zUP-=ftl;I{ZOBq8TNkyes^8#%-o1>3!>D&rr9 z4=bR`RHJe7T|v-s#Cb0Tux}{mXm2k7Vo%!4eWy#gFX{+z2loo@+oG9L!BL_hs;Wyg z6UH;Q8n?g>a=^VV19FKL;#A+#!IPq%x7TuQc3DqNiY#}1OYJw@RC>Y`{7Kyk=(Ij+p>$!CRm#7vvuZ$~HznNe> zL@&{xXNeI!VcUAaBToRgr^vMgNoy(rt3DY#Zw-f!FVw|)4(P_NGT|6ygGW;-PAJf( zRRg7|FA{xtV4K`a8(Mn-Y3Z{-thOuKk)R}iuSK#?;{DuvFe0TDY7rEYT!o8K#Kv+x z9?6u>)dPMLOuR=$JPyn41qsm%S?IBX5t-SO)a`;cacaH-l5QcH{V`J8WSBW zOTQbYsY(yxMv+GW+O}CDM@l3S7UD0#1zrhjqF>#Y)#JYP0z?2e?kMPny0A~!h(rew z7UG;;+^6ZVlj!1IkIFGgR&!7EjlD&pmpmePL~r(UlEkQbmx>(mtBztfo;zljb*sz3g}cEu zrWC9<*;dmHEXt#b0x4*?m^nQx?hBwNc7A|c?o~C^(w|P8lvp3T?JMvXzek**`iIEE zbm~;OC}oZLWKOv=DjvH=bhQW(DXMN;Pn}x4^$n_L!!QN%h~znc6c#swIwfu3^A3Ag z@%W4k5JjxGq*&Rab9@jb+(*zG)hs3TW|2Yp?1thkZi9NJEBnTfz&Uv;eg|@Z2o5R5 zu*xO-67aPhAjL?L@nPG_JwlDvDJmq0vW0QsX|6etpxVZ2;%-aq_=n|?(@8y@_SRi~waOmY>1SU&+35hJQfVh~}NF;3Q4aLqFmt0p_&lFC|c!pHVTUeQXbKS0~ z^(s1FMS1>M+6ah6t2GZGTZ?&l_dag~9);uy4TXHTQ9;&aT;(zfe#6EUs71b5co3z^ zA5;8+qF zuNgI9NzIKYVdGZfH2OU}-M?8Ycr+A^=vE3&R)9Jn7Fm!YbnZHIiY~E3pVQTRE&7O_ zdrJV6CiJ4~LptkG_xTX!$S#SiT)-vY>kIM30G^C@JS10`+oaz;L)YlaOe;}qpfbkPXiVQ1P; zw74>9c; z*lQOs*w%xHkc&qR`Gi5|T%v|bb@8|?em4)xs3P%X3(NOGxEN<05an_jO-%zs+!i0h z^8|=C@S9zRC>1_$aCYTYah!9G+ZiksVRe=cRf-(#vxl&oOm;j2}U?d{mvTL zR_Uu2vc?pr*hOomm!VloJ$QmjyauNg1Q0n{wxloPmdhW6F@AH@isWOSOn3m<=32 za0+#eq9Sz)K%=6THkXXtH!dXHuUplL$6JFHwuL_?(g~6`VXm%=%aHv>DuaT?Q%UYs zyIaM;!d+ONDkAGC*Wn$kj-fRD+%iq4Ys080E&;X6k=NJPIV zSX|Mjdhy+cMctxmsLX0p&VX)=VyNled8Q8Jdo+}2C9QTmXKT)LloO;>O{*32+pe8x(i5#zv{*sZS87`1SKjQcMxttjEqPLjV6Yb zC($Eb`$!M^n=jG582PQ3;s_26iFnh{0J)L?h~J=U6<*v0jaW&ka@N+GM*BvF8kuxt zif@d9U9peoB?ax9DkVC50hHG`=tv~hBVg+Nw4*8K#HW^!gJgLOAT=ViWVU$y#Hr{o zU{d8Ycb@x?So$G29;S|W^P4CvpPdY4TZdCU2I9}ua33D=1N|FvdOCgFchFD^`XJpp zB`mP%w%UKJMWSR#rJ}eFHof*yEfh^Oi(|Xrq=8GS;%5Ao_#`_=Aoe}fqoZ{KSI9YQ z*JGI$%MuU7jf9roMf(PZRw9WfV3}L`w&5w_UI$wu-bu|=d<~CE)28nWmQZ)1!A}Gm zxRsK4G~bAQ98K&UBD(7gQoV?1G?q5v|J3fhBm+~Tji5Ma^;;nyBZ)#+{fqb=Z;axu z^@0|U^M$k`YoJrrfyfUWS+ZJI(hE$-wikg4cRHb`kP6v+jkm>w0pf+HS}6{>jbZfc znMbXVXHd!Xdh0=HTz=Y>3qSK zcOlwFHe^AN4<-!wuV+^ck?5yzwnxs4Qwo*=(MY3VlCksnUZ~SO> z@QHrM#)C&KU8E&d-YnmVQ@V%)0&C49O!Gy{>qu56(Lz9kKR}-~i*d?bCOS&!Mhgr5 zon=~HpyTuo*!X58S8&l$M&06dj!@tvh+8}ghxEdoGP5_fxy2Ig^d}v8*j3`~0W2lY z*rG(Hh2cnI;!-(0BFB(Kox7QBtm+lt|2rD~wsgAWO`36BhuTc%)22t<){<&}QVDEv z8#Xql$~gm{E6G3~?)Z_7Bm4+nqvu5hBPu9MBdIbqSFsvN4O@+8F&x2fRWv}U-_0Ju zHYHJkLRToVN~8_uK0~O{St>=6=!m$xS>19J4V^`0DY-vKf&I->hQr&za|leR*O&@K z51j7v%b<+cmc#?dtly$9VBwan85Z z!UQJqYOG92_Pq*LMDSb#?Dvp;^qjk+xv!}btw6M%R1?S0$oW>|rhGiT=ligu+7`zU z#lSZbsjjG#2Dz_fPMOe^O7zC#rWCB`ODaK0UAtW`1EfkcC79&{>^v2AYtdJxN@!Hl z3wy48<(s0{@-cdhrka6>c2$Xvcit(T8LfgwV>xY_Bzf00B12zK&6NQ6wy?DCm=fmz z-oBX~vx!N)d>QbAUbP?8%bL?ZMMGpIU9~JB^lN#AoYt7ob-A=tjH}}d zM*-cJdmaDweo{EZ&tzBEnaF^BTTk&c6=;fiPV@XUZ{#R4)4&YyZnCgZEqvwLxW&YY z(vT|W9Io2ZXlParq>M+La634+w+alcPgmbRN zmN>ko(L2{AngNn}7k7o98nw~JLxyxQjkXfHa0S7()gzYr^^`fKoW7b@@4T5v{`tTs zM!!|hQ2Tq2m1UHMN>Vw#Ct;;Ia>e83rZQyh$^_VMXM4?E`Y}F5>w<{71-9kEQc!F2 z1mvcV9~G+j0)aFiD{cDY%UZ z*lhYam-4Q+A%Y;q3m*}XM7xpYEHXq586iOj%M#`k?0r|3>98SZn$_V)mOw@zMY&|= z6mNQg_ZM*=x_Udm`xDe(z=SG<$#q>`o@CM^=y*mTP&}kV$PDH6o}d}C10n26_V1xOuR8@ z^uPDAor&_=`d%W3@r|RW+Ek_k9pixx5zvLxR8ldieQvSuWlr{pGGDp?nWW^oZ&Mj3)n5kjN=~P&t`D)lsL3du1Fsn`a1o7~hz}L{ zDVvs@OM6mW?rUZGsL@Z4SGIC=)}k9T@w2J)$W;;jv@NWKpivhA_3CTwo}Fdn3A)%H zHH+8?+Y+ga3kcG|v0ofB#}75pihYHPc(q25 z~h7bDWbx~e#Flr(z(N?k#Vc^9!I@3goT3HH#I!WW(QLbO1-18QyUR0zh zM%fTw)e{Ok9WfWQl=V6^Jp67#PPG{Jfs+{k_!-tSZz26CDSw+{1FX+R(a{B`8mzNc zZF8p8#@B&Uw5U%X;{!;~5qWzD@$d%eVcZNj;-KTyVFx3Cm?oEkD%oNdq5FzBMYWKP zA4if*qQItXiIxeGEFu%OI$uLRFwR(_%|0r~xrgP(4F%QiHtJ^N^&E85J75@Cil(|H zX_}8M+;t#sZBqVNx;o^PT(h$@$M=e`r_34f@S@_B;FPOG{))hd-Sq+sttGYPHHuu( zIu{V$nKBb`f&JXdk&2G6fN2g{FVc^XVu6zq)yxx|z`W=qI0^}?4OdFh8=FxTukv~R zxaoR|(t%B}^58zq<3cYkt3j$hreN%xf4Q|5Y}$q8WndkKT-#oU+{?B|hEK?J$QWe> z)p+Y`yWi$Ir7`1+or;Hk57;B;J}xVkwq9K)>=8I600BMSOzB>*$681e4;>f5Q!Lwe z6>x$!;3}#0x|}d1&sIn*s!SAU)BVNH!$(aqiVWgvREf_(M8Cz1feT|J&)QI0o!c{V zw`1apxxHN!4IAGDN`Put29Ds^kWUBH%L>Bl@@PNy<(GDqkM z26pSU1Qs;xQ5>VSnCoe8fo(A=gdfYuc|(EGbyOA>1c;8JwVSQ5BTb-pdSyI=C>`3i zvX7Ak3sU9wA^bb_p7CN)QgkP~m7~DM>Kt6C@h1~>+%+r}kBfPEW2RMw4nc0rp)% z@`MFAt!)_)$Bw{E% z3gjBved>+}93D;KjzYzqpH-DRvuoS=2Pkau>i$q=rvoZqptSOWasQ_TiC_cVk<}OVE!B zQK9KMt|H?$ka}XLE`h$LCtZXE5g77a(_1h3$Wx%F%90wfZIXCM5$c>Nh|(HJcEv3` zB-3G~=wxdeMIg;9BS=xd#IG1-Xt6qJ1+&l+D=ZKS{ig_-(!`eGXHy&!Rx~=$pV61`)M5v2u+Xx6Mr?_DvD--Mx2w z+q)A&l@gt#2G14U4X5|fsa9Q)>ol%`1$hA8n-qU(cA0|O*FBN~}1(hI}s z_teaokigfqP|v>V;AuS7&Q*APi_N54M5kP|V4^welpP0XB_oGNeF?ajJ8rBFUW+0O zNu>%6RCXOOB%YJK?;PXmcN*R7q|HiqzpjU(+s%BZq(GPAh}u~hYuk4}nOYpGC{GMVFM4q8?$k_h(XJJ5(O#zU z|GtV%9O(+6dP?xDq}rn0h{m&dM35zJVP|FNIB97o^X}iokuH)$k#0lQaG~e+K*W;x zhWPU#`*t&b>vS7BnNj&J{?_Rj5)V>xZ7Du&8?ynG1j5I)V8Ceus{0zuq<`bNy!$tC z8)8jm5RG3!BMi!1wkg#b6tc?(v%bE#(`~2)2BY%K{qCCHRduwyw-!%eig>Z&65U{M*^ns?`OB z+VnSYFO(Y!G9LOJx1!oc7v8+%6qw7}-(?db^jb!?ff%10w$h?sN}}1FT7>Q?O+LK(!>tmZL#P3H_>g zw`o(CjkkCYR$SvPE(2668rMOMhFZ8USap!{uZE5K2K!c#;RP-;Z-QlLwInMhL*THb zoTiC1r^6^1wJw{CjnhOQyq?XOc#HitVSLs24SF3L-*#Fw#1GX(wOHI6TKLiE#ta$~ zuAkNYc4g2!*diw;mLYn|C7OY`G!YeRvi79e7z5eH!oV~VW#fG%U?ok@+YsVef!?~c zr9re+MhIp`cj_&<#Ea4c1G+mnZ@aJb;t>R~YQ!h35uT>}m|9So6Lg_?Kk=SuF8hK? zl~*g>EdoOVT~i^v4q=*k0SQQja~%;yMQ*32zvsr}x~QKid5~zlblupZX7K27l_Lc_ zVOKvi?W*jzB9m?=%#a?w(f37P8E?Z0bSgfwgd3$8is>b~3~4D84dqc6O*I!9=h{|W zhkd}*0)A`cV@EKUYV^+5aMOALv0wR0L`yp%qvG)dTAzj73?C0#s^PifZuo&~N-7Gr z3YX~oC6l&hKRJ@a$|IuWV3Ls&5PUZ^;76)l+379iTWl*W$BG(_VqIEGC6J}YA8XXO zoZgkU3Jg&vN#{y@_!{pzh6b`C5tb-!WFqvZ!fjFuD5k$a;xgxhw~ZIqr_21k2cXGv z5*|&Z*)T7ift-`jkvS~dsmkVZFCcV8P3Z^&jGj`f$gWWV6T$#-3p!n9A%`9J<+JS) z*u8p)=<$Xty>Y{Ol2pqZNJM+D92s1|SK~BG{f-_TvEvu+B?SAR5@DfV2DTJrh6=HsCX}5@moi$|!$u!&*4IOR5_Z$$M zrvNP0qHyvjYs$8vgn9LSQ9@DTWuxt?0EVcAHg9#$FeGzHI|Sfp#T!aO_L~Z?qx(3-B#BIjwUIZ zB}3YE;FRcBP>G@?zGQcwo2{)&eH?PBc2yeEoOiEGTTx;tiIowsXi6nYT!J3f9 zC7>n!Cp;Pn2IrL()y@uewKtWRC4Q98+hUtiBHAc|LR|WkqM-#WJ{@@42hQzgrna0Y zj$SjMSu_-IQd@~|$bZ(9UiRtk_+)fblmq%fgPEW5!#RytQ%g>McbGPwF;?G(wfel@HYmwL|CxC zOl%;s!u9-w-cn`2uUR2d2EF_$7!(mtm4$@YOiwx8@*`qI`lwMc&_eLD?QC-VokokC zHD>TZ+e^)~Xxc7VUq{R<0+39AuJSjnjJ_o^Q>Qeny3?8}YNgWwZ|~tf#9C7PnqA{Y zAk80n1dkj>SeNQqNH^_VE>s!Vk;byvklWGli^vm7wC=|CwG3FPj{j5{FiUPC7; zxu?u>p+WpFY@6;SpkZ+kOi!JZex3s#IVlz5M$u2S)go(O)^%qWbzP2#3U0RT{1J>w zh65Oj+4(s7(c-Bm;Nsxg&Nan%HpAzE9IrWeie$R0#2p1kK+Z+Vc5tKmFO~Tk_ov`Q4v>`KMpw*ZSS>|I>f?hhP5u=U@N(fBy9k z|M8E1`Q3LP|GU5Z&wuGZ6@T&%|NKAx>JR_<&wu=Y39Jwc{&3UJ5!5YokXgqtQkh(CckaI#tJ14}9H}=;tl+ zxJC5>KC;`i@U>lzp9@+i@Q_3>tY-+=_tU2V+-NCmSyR64^`~E>mLAr@i+R=~d*Hkm zdT46R>)bf5og#lfmFgC~9-bBX0f!&~Q5~-01RBL>P30_4_CJf)S{1sj6EE~%9ue?| zHDc7IwuIftHi_-;FjNFTIyaJ$c0WuuU zeYdwceJN7&Ts*;3)KJel{bNSLCb^oP>NK8x`Vx`futgeLia#+IcdO315?|y8rCkm` zpGfadQ5v;{EAUUr`~GcYVWMo89Ddwz9_#JJB8#<%$IbZIaLj9d+vnA@swJ zrqg1vU7Ec{(d^SRs_9P|^T?6#NhlS(=>n+^PG*1#c`i}sDPPlhARFIEfWq@JaB?Sh zdF|sC(?G7D$DMf(6H`Sl@Ego|QSB!(K03i?hPDrF8wLHtn(#c(D}8|5lBU8Wc*Sl~ zJR5yV-|EhZ@&%#3L0pWs9P$v`HZvY18dX>org=R68&BT9sq!Q+NUL75l_o37scE}b z{(a>2$ko#aL29b+xlP7L$;$S#wBboPKh>`K9aGDa^5rRCi!+-=f4j0z6&hGJEbJB^0~yP@;-z8NNpX8 zRO8X4kmYITPag=BbOCQK@9{id{b*q+@_B{j#-_HSB+-~u$5rn`zD?MVmiD`~4xZKF zkEoxI-i@6uz;i@MS7ktwRepOgc*)Yokvfuf0qDSu039$6rbP!HmH4iEox~_NrTntU zOQ~K>&^CON;hH80+NR`#mcom4W^`llYqlJ7cD&k>hhq(yoCWx0HE=-%uR$4lj;8`j9niZKM7!vtQ~! zNLoB4z4M&6J>q5`9kQf+DYlo-^y0iz1**z&oExP;EsPQ6N!xOTyHT8%lK9kQp2W!~ zXyGi(uEF8y6y+7Y9=RpalK)0xUmSk4o<)tGZ1}P5a`M^me0H@vtGGuR+gr_%TGC)I z8035Llst+qwo4!`6b!QQqi<&7^F7W+kj>UZevVu?3a`^%d>o#ljTO>k_w^;-*xtwE zPP;l>VK-PJGjOsAfL4;r8=>{3P=7B0BE_Y*+F8xrU$anOUf|bSqT1AhfwL&~v=+pO zhv=5sVanyHTh;j}JT@1n?e9me*CgV8MAVlbJgSwr+pfUhP<-rxd^UOdVQaja2j=2&OynigR}54S~> z{Q-K|7b;QEx`XqkS|ZWElr+bYRIp%#xnWZLkVu5Uv=>~w^$J_X`xaf6`lK7b@GHp^|=z7%7g@-rbUZ|N7-k!vfZF zlGx#=b2U>=D&j7h^oxDdKDS0B2*{yR-&M-ixNMwJtY%49>v=BW;3Y9Gq0zgGrOmQbZeY=Y`y}V z1G$~3OQu<`@-)%uzFyjHHPMmMDv#oa&cW!6eJvrTCrG^nqZ1!nfGnbY*8^Q~E^BE~ ziB!MM$q$KzbL#u|VBbW29}R4U=P!K}_$eHzL`&A)X006&R5^2qi$lT5HR|iM`Qg_e zJx3`Uhu($(&AhzVc-l-jU}}prPkWVktyRM{1@))wYLA$onzM1!>j(N%qu5kWUNuhn^Z8#&=mbx7YJ-P>Vn$a5n3sn)$+x^IMH` zu((GKT+@nw&XRttZW8_Htg6>2D)sGDUH9}LDNh_tz%!SrN1~Xv5z>rP?;^U%Lxp6YSF>HeD)%6T!L4)I*Gc;m?U8u8q?-4(if7%t^=A=;fM z3PXEgv#Jb)v@}NY^N5z?^es|pRN1c&_|!>-;D#d0IWAMt2!8g2WUI8*d;et8@pHrz z;bV7`)AQFS`bXsVgyI?`8mY3*9;4(=UlJ|6p610D*oJrI<&+HnF1An6*C887#4}mB zwmhP!FAW{^AZ5)ek*2#Gc`MdcPNJ#rE>|ZM_I%9lGs9{swp?-enY__zvVGQ48p+Ah z=UhC;eKd3c(kYmUBcZYTc`7<%niF3o@7l{FJ!{H@VLr>{$#a~%DSN|Z0SHL@uq<2O zqC!>g*oB{e^z!l2*OqRuc1^^W_!biV+~Mmsr%js{{!J=#k>hbA?X1%&wt7Hu5fD*; zu93D2g=YF`PW$!Gbty_?m0CBk3Y^+1iLH#JqJr@zh1FD2cSBPv_R%gR|UXjBi~ z7EW;mYZnxKFZ?w@9U$~3n2*dPVjUw-&esTK7~ z#t;>KHgeO&Y}SP)ParIhNc;QrdPgB{h|%Y?^BgfO{z|(wxWp5ob=zCU5A(}e#YZ)s zMpT2(6taPBqxl|pwsmU}%%UJVOIU+@bcvcGhinKJWc(IwEpaZWS1iLQ3ZfN^r=Tqw z{p8_HM?AQXJUR{#UD`V*E%H2=<|z10IbJN4mk;*E((_nhEs2Sug`SoKz_`y_Zv!~X zQSU>YbK-uAU~1Q3&Qf2EFJRtL^zU)8@d?{HA$nfIxgJz4a2Lk@91-XSfLTuXRnhXFgq`|oD@XxmnLRuB_IutcJEt)x5_!rB))`%m^- zpSimkL8E)-rA9 z+f2)R&#p&^o=kSO3i>Y*N(QuJs&DE9S9oAa!U>|$d?F3ocl{o*(ApEE=7TrlIx zKH2+@s~$S(@kOm&;DYocD#(u?(o<&HXs+8|%gFj&C-y}8^?@1CR3pAXCOvCDP|D#F z`?I3Y>)2~AIZ9hqeS80Rm1mP)#|KHo9St+BQXwz;ji#lv1l^b}ZysDbl-c8UD$ zM|(P06SZM}VLL-`qmGXtFi|nPrq$zO=qWF<#AHU-TTD_n+H%q}r)k4JvJ3Irw^t1H z0mj}4F&*XnSO+uKj2^5SvC`9}yGM9j(0_&x$l#fAy($rAY>c5LvI0`2nhw?45u-+T`i zV(BF(KWO>cKpi$9Js6C$oceEs`uvz>d z%;h#*_j&1k=wLYa?+fRNZH!Fs(M9A`;Rt|UL1@ngUhn$4a`UGIQ(IMOitlNZ!+@UIq zzFGzG`#gPt?3^O-iX#;JYDhN6(Xscl!em|_qR51T&(gQr?5DJU$C zPmuf@&GX~*#a;s0WBlM~MKAUGO|GbyZ*Tn^vFE9&uW>h>>H^4(`@HoqHu$NE;b*+_ zNpc(`ow#5cuhk{Q9BhRlLb;P7W5jb`=qFJF`H&HxR(kt5U2}K#?Ohmlf1J}+c*jsL zl~3C1Z7+ceV=84DYV(LZIxqC7#I=Z@9Ii<772Y-)3|xSugDRS+oB`aSV~x{wG!3so z>}3gO6ym?%Zif$Wfyn+hgI z8)&M1z}ewF)1bdmdoS?G1(8m(x2I!?8{Zn^qP_mjPtDkDW1ncmKbQ)oNoI|rAh8D1 zBe8APcmSmubR1>#DEQBiUBYr$pQa=^wg@v9F|i|^M|fjcd*3?d5P+MZQ2w0lt+|!v zki*NuMWofU5XpA18If|{hqTkuPJSpjE>}{woEGX6#F9YW6 zu`VY(ZKo!`_Fj(;vuE8@Xi*S~v%9U0A1M+!#w!r1;fY1yK2cwbH5wSovEV+V%RdZI zBb%m`{56tGSf!+#PE_%05U#XVs#N*rtud8cYWoCz>a3a69BxTSJ8hMBOm-#p909J- zTl0b#9r}~0twz@y`_MAEkoQHdQ$r#?%`;`tKrG8`oqyM}g|%BYHXo+hYJX~twbh2)YhgLjvNBTTkx$gn5ypY(&j}W?%pvqo;nGB#xB># zHb+ytu;C>${WL|5_db!MOHZ%ftHk4b`D;&6)}|k;JjwVXRy5@RtsCGk=qeilI9_9;frIOxI(Jd-BVBR$EqyY( z_S4D|SA+^gK^P_f3gLsY4^E%I_sctNuvb6)?^Ey$FF@=j-7-pjtSG=qM_C~CnK;Vd zwJ|tuMAE?Skm_zjV9ZXF+B?R2@e$SMmDb6{UL1dBaf=q;yJy}Ms~?Y?Jqmro9{A@X ztJ|#eC|zZmwpODOU+~ZW=2CxHq$VjK9{)9XZFY~Qh9d1)i$-of)CDYY3>rOt>+k*? z=dsJ1uZ3VZTCpP|p3&hCLSB;oI2uW~L~bi*a^y7l(iJ()`4D_hs z<`fN4C{YVywggL=S>}zfJ45m`@}Wk_cb?APDD;z(%TefG0NjGq(?V7{tg)w$Tl0oQ zH}J$mg>@rebp4K>gJ(>2k76(Wl8pYBRC{T7ug*4TSF7P(eAVdwxr!{S0>Euv{Z2#e zyOyCKe#Y4!>zp=#GnX9w!;_D~Oq5p;WxdU@h)(N8UcwHyIeBmV0^p7vGKmUB(AOG_I{qDQZ|J`5y=fCtHi$D5@ zfBqkT^@sob=Rf|JKmO_W$B+E_kN?EK{_zig|M$QD^Dlq+Z@>OI{*iz8%U^!^tAFOd z=4bpL5941Ir{d^w*bO0e?C9(smU&)st0fDoZ=#Z~xU2t)pZmYRukk)$JFwrFA2Yh#FIJ6(=^ zG8ttgKE_Z^zZ_nMb}-bG7l}xE#pUo;mYTuc`24Bg^UkpX>sJ!OKleIt;p5+x{)}Gf zqU)gYTv6@+-3;`xZ+O|&h(j4LMivLJ*Pz#dwa8cbA<{$RFV=lK=Md_8@HrLkR$8au zjAIx&bSt7XjyH|$^|}?1Q0}h^&PRll?<2?UZbwfkj!xvdT2ub?+i~@d>_K(WYeXm9 z@556qx1BGen#w9wa!gDhN6H_OIH~Z6gI> z@xB&$#=<_K&P?`8A6_&NpTDSQcOm@s(1QKuEo#JwE1Hm_&TsJzSzm9X8$Adl8UKqG z6|rdlMHO5qB{}am3y1y^Tc@+&O(qyhqy97lfaH5jC4BVGX(gp@Ub|1Vk1a{l$Sr7> zY)Rr~*jLM;2oo#p#m>X;F7fmcPx%E)vmx_^Y5W}Bz$Kk3%RqoU3lxuz1pN|uv2zlE z{?25Sj9{*>Sg4N4YEsiUk?PmN`+2&nR;Kof)G?|}i@#DG@Di)9 zIM6T05-0`kJk*M`-b1kIA%d}xen65cPtb5c~C%-y!mTRH_y zRfWdS+!UqdS!{PS9dLY2xycq4)Jm7te}>z9q#Z#IEv*`{>)d)_>+MME15b<(L3emT zFY*rl(iDw`*5}iz2ah)Ox65(2^3w+GJX+MoNPdq#_0fe6=1SBicgFjorWx&X6Y8Jc zsMZ62p(%Amc7oMB(v+5Fu503Y_n91l~XjJe3Ve8nsKp~dRn5rVnzFx@Pjd+#yajC1ENmmKMj zA-~n$)$f-*25K)~jMOuH#U|unC`UDcaWI#(rU>){d;sJD#rdF1Oii0MP9+FQAj(uC zoa&gnP2DWqUUeY`{suAq$Nd8TtU9p9&#O(&F{h*`G}g5Y z)7BGk{mM`6XIC2bA-@ex{34n%e@##C0?VK2{cLv|Wz|0BeEM$u>>;=wvkS-vDsqt- z&ySQEJorZU9dy=)uSMh>sNZ9Q7M~iyxV|3ti}ldTdA2phh;#Sek&yeaPUh%k8{R`I zr;je?b!g+!xUvj8*V{>=0BN84D(4B^WibLwU#>qMazg+g&`W2Y?Wj9bV%tD33J&K9Q$&Tc4nao_5*#0)SI&p0s444A1v{{jTp;c}B^2NJQ0F4D^xbh4)!2O>8VT}$*y zTnLh;Ucv7n{~Tqde~Gnt7~JqaygER9*dHEpYbkTE{MeJ%A-0g|lzYo`LKjWyGf4GU z-3S~z+g9WK)bw9``HkR%=k{!E{y@?j#t8bJIdv1F^7^cy-F#z8qxz5B0p|7!n&B~> zAZ7B%-xrNG+xMbaIPXZ?tvgb{(z>U4Jf*xL)?VN~LkBu5vIim>Ut_NMwFXKY_4mhq zJ6*Mbl9I|%chXDjXWl?bEBcSG1)X(FvcAYFJGZ9VrF|_*!5WSGt0q}EdJ!~gYdqMg zvkL9b0B-rsS2pYQhYTgQjU16v{k8P<_h2ix73v1z<~1m$No{kWYRzAO(|QMfz{>Ct zuzp>cZhjlQ^HMpEhTE&gSi{r1ipIZEtz7{5X896rfxvfk%Y7~QrhV{~*(h4#i}bV7 z-oJj)UEsZmsL9gKjWz^+)U^c7|7c?`n4s3OmJt zexrLt@BW7>`1Z0lbdrgRF@MG!Y&U5c8bWq+wlM&c{VAn2wLp09I4QEEuutS;fvX>(}f;Fu%EX zfOfT{O7RA#!}ZH@%keK1YN_w{`r83IHZsQw6=u5-`YWP3=-s-L)>k1Pnm{j#9n^-p zMFcLz3a?iKw*@xuhSteBz3KtNkfRFYmpKF+0V3ipuvCA&==TpK2CDdV$FlcW3ty?q z`6ePK2sqpVp4F6m5Ql2Su+)o`+fBV|XRK>Quvdt5{zOjM=fC~*XW#z+ wmW2gpo`VHvb$AN^0R#X5000C40000`O9ci1000010097L0001Sod5s;0HJsb4FCWD From 24fdb82d38809dfa61a5df7b22140c5c3a068a97 Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Fri, 18 Oct 2024 01:27:24 +0300 Subject: [PATCH 04/14] header resize works with bugs --- firebird-ng/src/app/app.component.scss | 12 +- .../display-shell.component.scss | 5 +- .../nav-config/nav-config.component.html | 11 ++ .../nav-config/nav-config.component.scss | 18 ++ .../nav-config/nav-config.component.spec.ts | 23 +++ .../nav-config/nav-config.component.ts | 37 ++++ .../input-config/input-config.component.html | 14 +- .../input-config/input-config.component.ts | 3 +- .../main-display/main-display.component.html | 174 ++---------------- .../main-display/main-display.component.scss | 25 +-- .../main-display/main-display.component.ts | 24 ++- 11 files changed, 129 insertions(+), 217 deletions(-) create mode 100644 firebird-ng/src/app/components/nav-config/nav-config.component.html create mode 100644 firebird-ng/src/app/components/nav-config/nav-config.component.scss create mode 100644 firebird-ng/src/app/components/nav-config/nav-config.component.spec.ts create mode 100644 firebird-ng/src/app/components/nav-config/nav-config.component.ts diff --git a/firebird-ng/src/app/app.component.scss b/firebird-ng/src/app/app.component.scss index 2746449..139597f 100644 --- a/firebird-ng/src/app/app.component.scss +++ b/firebird-ng/src/app/app.component.scss @@ -1,12 +1,2 @@ -//nav{ -// display: flex; -// flex-direction: row; -// background-color: #2e2e2e; -// color: white; -//} -//.top_nav{ -// background-color: #2e2e2e; -// flex: 1 1 auto; -// max-width: 250px; -//} + diff --git a/firebird-ng/src/app/components/display-shell/display-shell.component.scss b/firebird-ng/src/app/components/display-shell/display-shell.component.scss index 79a6eb7..59d79cb 100644 --- a/firebird-ng/src/app/components/display-shell/display-shell.component.scss +++ b/firebird-ng/src/app/components/display-shell/display-shell.component.scss @@ -15,13 +15,12 @@ align-items: center; min-height: 50px; background-color: #2e2e2e; - border-bottom: 1px solid #ddd; - + border-bottom: 1px solid #1c1c1c; + box-sizing: border-box; } .main-content { box-sizing: border-box; - border-top: 1px solid #ddd; display: flex; flex: 1; overflow: hidden; diff --git a/firebird-ng/src/app/components/nav-config/nav-config.component.html b/firebird-ng/src/app/components/nav-config/nav-config.component.html new file mode 100644 index 0000000..c18d53b --- /dev/null +++ b/firebird-ng/src/app/components/nav-config/nav-config.component.html @@ -0,0 +1,11 @@ + + + diff --git a/firebird-ng/src/app/components/nav-config/nav-config.component.scss b/firebird-ng/src/app/components/nav-config/nav-config.component.scss new file mode 100644 index 0000000..7bbcf35 --- /dev/null +++ b/firebird-ng/src/app/components/nav-config/nav-config.component.scss @@ -0,0 +1,18 @@ +#main-top-navbar { + display: flex; + flex-direction: row; + box-sizing: border-box; + height: 50px; + color: white; + flex-wrap: nowrap; + align-items: center; + justify-content: space-between; +} + +.config-toggle-btn { + color: white; + position: absolute; + left: 0; + top: 0; + padding-top: -3px; +} diff --git a/firebird-ng/src/app/components/nav-config/nav-config.component.spec.ts b/firebird-ng/src/app/components/nav-config/nav-config.component.spec.ts new file mode 100644 index 0000000..2ff396d --- /dev/null +++ b/firebird-ng/src/app/components/nav-config/nav-config.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NavConfigComponent } from './nav-config.component'; + +describe('NavConfigComponent', () => { + let component: NavConfigComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NavConfigComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(NavConfigComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/firebird-ng/src/app/components/nav-config/nav-config.component.ts b/firebird-ng/src/app/components/nav-config/nav-config.component.ts new file mode 100644 index 0000000..ffcf020 --- /dev/null +++ b/firebird-ng/src/app/components/nav-config/nav-config.component.ts @@ -0,0 +1,37 @@ +import {Component, HostListener} from '@angular/core'; +import {RouterLink, RouterOutlet} from "@angular/router"; +import {MatIcon} from "@angular/material/icon"; +import {NgIf} from "@angular/common"; +import {MatIconButton} from "@angular/material/button"; +import {MatTooltip} from "@angular/material/tooltip"; + +@Component({ + selector: 'app-nav-config', + standalone: true, + imports: [ + RouterOutlet, + RouterLink, + MatIcon, + NgIf, + MatIconButton, + MatTooltip + ], + templateUrl: './nav-config.component.html', + styleUrl: './nav-config.component.scss' +}) +export class NavConfigComponent { + isNavConfigOpen: boolean = false; + isSmallScreen: boolean = window.innerWidth < 992; + + @HostListener('window:resize', ['$event']) + onResize(event: any) { + this.isSmallScreen = event.target.innerWidth < 768; + if (!this.isSmallScreen) { + this.isNavConfigOpen = true; + } + } + + toggleNavConfig() { + this.isNavConfigOpen = !this.isNavConfigOpen; + } +} diff --git a/firebird-ng/src/app/pages/input-config/input-config.component.html b/firebird-ng/src/app/pages/input-config/input-config.component.html index 8cd1962..db310ae 100644 --- a/firebird-ng/src/app/pages/input-config/input-config.component.html +++ b/firebird-ng/src/app/pages/input-config/input-config.component.html @@ -1,17 +1,5 @@ - - +

Configure geometry pipeline

diff --git a/firebird-ng/src/app/pages/input-config/input-config.component.ts b/firebird-ng/src/app/pages/input-config/input-config.component.ts index f704108..5d034a3 100644 --- a/firebird-ng/src/app/pages/input-config/input-config.component.ts +++ b/firebird-ng/src/app/pages/input-config/input-config.component.ts @@ -16,13 +16,14 @@ import {MatTooltip} from "@angular/material/tooltip"; import {ResourceSelectComponent} from "../../components/resource-select/resource-select.component"; import {defaultFirebirdConfig, ServerConfig, ServerConfigService} from "../../services/server-config.service"; import {MatAccordion, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelHeader} from "@angular/material/expansion"; +import {NavConfigComponent} from "../../components/nav-config/nav-config.component"; @Component({ selector: 'app-input-config', standalone: true, - imports: [ReactiveFormsModule, RouterLink, MatCard, MatCardContent, MatCardTitle, MatSlideToggle, MatFormField, MatInput, MatLabel, MatAutocompleteTrigger, MatAutocomplete, MatOption, AsyncPipe, MatTooltip, NgForOf, ResourceSelectComponent, MatAccordion, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelHeader, RouterOutlet], + imports: [ReactiveFormsModule, RouterLink, MatCard, MatCardContent, MatCardTitle, MatSlideToggle, MatFormField, MatInput, MatLabel, MatAutocompleteTrigger, MatAutocomplete, MatOption, AsyncPipe, MatTooltip, NgForOf, ResourceSelectComponent, MatAccordion, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelHeader, RouterOutlet, NavConfigComponent], templateUrl: './input-config.component.html', styleUrl: './input-config.component.scss' }) diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.html b/firebird-ng/src/app/pages/main-display/main-display.component.html index 6445f2e..aaa711c 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.html +++ b/firebird-ng/src/app/pages/main-display/main-display.component.html @@ -5,19 +5,6 @@ - - - - - - - - - - - - - @@ -28,63 +15,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -93,84 +23,26 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - - +
-
- - - - - @@ -179,27 +51,9 @@ - - - - - - - - - - - - - - - - - - @@ -211,12 +65,18 @@ + +
- + + + + diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.scss b/firebird-ng/src/app/pages/main-display/main-display.component.scss index af5dcb9..b4e2840 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.scss +++ b/firebird-ng/src/app/pages/main-display/main-display.component.scss @@ -3,12 +3,7 @@ padding: 0; } -.navbar{ - display: flex; - left: 10px; - flex-wrap: nowrap; -} .phoenix-menu{ flex: 1 1 auto; display: flex; @@ -35,9 +30,13 @@ .toggle-btn1 { color: white; - } +.phoenix-toggle-btn { + position: absolute; + right: 0; + top: 0; +} .tcontrol { width: 40px; @@ -67,18 +66,6 @@ mat-slider{ width: 200px; } -//.phoenix-menu-toggle { -// display: none; -// -// @media (max-width: 767.98px) { -// display: inline-block; -// } -//} -// -//.phoenix-menu { -// @media (max-width: 767.98px) { -// display: none; -// } -//} + diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.ts b/firebird-ng/src/app/pages/main-display/main-display.component.ts index 468f183..10429f0 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.ts +++ b/firebird-ng/src/app/pages/main-display/main-display.component.ts @@ -36,7 +36,7 @@ import {EicAnimationsManager} from "../../phoenix-overload/eic-animation-manager import {MatSlider, MatSliderThumb} from "@angular/material/slider"; import {MatIcon} from "@angular/material/icon"; import {MatButton, MatIconButton} from "@angular/material/button"; -import {DecimalPipe, NgClass, NgForOf} from "@angular/common"; +import {DecimalPipe, NgClass, NgForOf, NgIf} from "@angular/common"; import {MatTooltip} from "@angular/material/tooltip"; import {MatSnackBar} from "@angular/material/snack-bar" import {MatFormField} from "@angular/material/form-field"; @@ -48,6 +48,7 @@ import {DisplayShellComponent} from "../../components/display-shell/display-shel import {DataModelPainter} from "../../painters/data-model-painter"; import {AppComponent} from "../../app.component"; import {ToolPanelComponent} from "../../components/tool-panel/tool-panel.component"; +import {NavConfigComponent} from "../../components/nav-config/nav-config.component"; // import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; @@ -56,7 +57,7 @@ import {ToolPanelComponent} from "../../components/tool-panel/tool-panel.compone @Component({ selector: 'app-test-experiment', templateUrl: './main-display.component.html', - imports: [PhoenixUIModule, IoOptionsComponent, MatSlider, MatIcon, MatButton, MatSliderThumb, DecimalPipe, MatTooltip, MatFormField, MatSelect, MatOption, NgForOf, AngularSplitModule, SceneTreeComponent, NgClass, MatIconButton, DisplayShellComponent, AppComponent, RouterOutlet, RouterLink, ToolPanelComponent], + imports: [PhoenixUIModule, IoOptionsComponent, MatSlider, MatIcon, MatButton, MatSliderThumb, DecimalPipe, MatTooltip, MatFormField, MatSelect, MatOption, NgForOf, AngularSplitModule, SceneTreeComponent, NgClass, MatIconButton, DisplayShellComponent, AppComponent, RouterOutlet, RouterLink, ToolPanelComponent, NavConfigComponent, NgIf], standalone: true, styleUrls: ['./main-display.component.scss'] }) @@ -106,8 +107,9 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { private beamAnimationTime: number = 1000; isLeftPaneOpen: boolean = false; - isPhoenixMenuOpen = true; - isMobileView = false; + + isPhoenixMenuOpen: boolean = false; + isSmallScreen: boolean = window.innerWidth < 768; private painter: DataModelPainter = new DataModelPainter(); @@ -141,13 +143,11 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { } @HostListener('window:resize', ['$event']) - onResize(event: Event) { - this.checkViewport(); - } - - checkViewport() { - this.isMobileView = window.innerWidth < 992; - + onResize(event: any) { + this.isSmallScreen = event.target.innerWidth < 768; + if (!this.isSmallScreen) { + this.isPhoenixMenuOpen = true; + } } togglePhoenixMenu() { @@ -442,8 +442,6 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { ngOnInit() { - this.checkViewport(); - let eventSource = this.settings.trajectoryEventSource.value; let eventConfig = {eventFile: "https://firebird-eic.org/py8_all_dis-cc_beam-5x41_minq2-100_nevt-5.evt.json.zip", eventType: "zip"}; if( eventSource != "no-events" && !eventSource.endsWith("edm4hep.json")) { From 7ff0ab46d2b0a1ac3b5f08b3a1a1aad05e1a743f Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Fri, 18 Oct 2024 20:45:11 -0400 Subject: [PATCH 05/14] Added test and demo data producing scripts --- test/README.md | 8 +++ test/open_xrootd_uproot.py | 4 ++ test/run_many_energies.py | 125 +++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 test/README.md create mode 100644 test/open_xrootd_uproot.py create mode 100644 test/run_many_energies.py diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..391e59e --- /dev/null +++ b/test/README.md @@ -0,0 +1,8 @@ +# Test scripts + +This directory contains scripts for CI and manual testing. +Scripts produce/download/simulate data mainly based on eic_shell and eic containers + +``` +root://dtn-eic.jlab.org//work/eic2/EPIC/RECO/24.08.1/epic_craterlake/DIS/CC/5x41/minQ2=100/pythia8CCDIS_5x41_minQ2=100_beamEffects_xAngle=-0.025_hiDiv_1.0000.eicrecon.tree.edm4eic.root +``` \ No newline at end of file diff --git a/test/open_xrootd_uproot.py b/test/open_xrootd_uproot.py new file mode 100644 index 0000000..cd757a9 --- /dev/null +++ b/test/open_xrootd_uproot.py @@ -0,0 +1,4 @@ +import uproot + +file = uproot.open("root://dtn-eic.jlab.org//work/eic2/EPIC/RECO/24.08.1/epic_craterlake/DIS/CC/5x41/minQ2=100/pythia8CCDIS_5x41_minQ2=100_beamEffects_xAngle=-0.025_hiDiv_1.0000.eicrecon.tree.edm4eic.root") +print(file.keys()) \ No newline at end of file diff --git a/test/run_many_energies.py b/test/run_many_energies.py new file mode 100644 index 0000000..7ce27d6 --- /dev/null +++ b/test/run_many_energies.py @@ -0,0 +1,125 @@ +import subprocess +import os + +def setup_environment(): + """ + Updates the LD_LIBRARY_PATH environment variable to include custom library paths. + """ + # Get the current LD_LIBRARY_PATH value from the environment + current_ld_library_path = os.environ.get('LD_LIBRARY_PATH', '') + + # Define the prefix you want to add + prefix_path = '/var/project/prefix/lib' + + # Prepend the prefix to the LD_LIBRARY_PATH + new_ld_library_path = (prefix_path + ':' + current_ld_library_path) if current_ld_library_path else prefix_path + + # Set the new LD_LIBRARY_PATH in the environment + os.environ['LD_LIBRARY_PATH'] = new_ld_library_path + print("Updated LD_LIBRARY_PATH:", os.environ['LD_LIBRARY_PATH']) + +def run_command(command): + """ + Executes a given command in the shell and prints the output as it appears. + + Parameters: + command (list): A list containing the command and its arguments. + """ + print("Executing:", " ".join(command)) + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + + # Print output as it appears + while True: + output = process.stdout.readline() + if output == '' and process.poll() is not None: + break + if output: + print(output.strip()) + + # Handle errors if there are any + err = process.stderr.read() + if err: + print("Error:", err) + + # Check the process return code + process.wait() + print("Command completed with return code:", process.returncode) + print("\n" + "-"*50 + "\n") + +def get_hepmc_path(beam, minq2): + return f"root://dtn-eic.jlab.org//work/eic2/EPIC/EVGEN/DIS/CC/{beam}/minQ2={minq2}/pythia8CCDIS_{beam}_minQ2={minq2}_beamEffects_xAngle=-0.025_hiDiv_1.hepmc3.tree.root" + + +def get_reco_path(campaign, beam, minq2): + return f"root://dtn-eic.jlab.org//work/eic2/EPIC/RECO/{campaign}/epic_craterlake/DIS/CC/{beam}/minQ2={minq2}/pythia8CCDIS_{beam}_minQ2={minq2}_beamEffects_xAngle=-0.025_hiDiv_1.0000.eicrecon.tree.edm4eic.root" + + +def run_simulation(beam, minq2, event_num, detector_path): + """ + Runs the simulation for a given beam, Q2 value, and event number, then converts the output file. + + Parameters: + beam (str): The energy configuration for the beam. + minq2 (int): The minimum Q2 value. + event_num (int): The number of events to simulate. + detector_path (str): Path to the detector configuration XML file. + """ + # Construct the input file URL + url = get_hepmc_path(beam, minq2) + + # Construct the output file name + output_base = f"py8_dis-cc_{beam}_minq2-{minq2}_minp-150mev_vtxcut-5m_nevt-{event_num}" + output_edm4hep = output_base + ".edm4hep.root" + output_evttxt = output_base + ".evt.txt" + event_prefix = f"CC_{beam}_minq2_{minq2}" + + + # Command for npsim + npsim_command = [ + "python3", "npsim_stepping.py", + "--compactFile", "/opt/detector/epic-main/share/epic/epic_full.xml", + "-N", str(event_num), + "--inputFiles", url, + "--random.seed", "1", + "--outputFile", output_edm4hep, + # "-v", "WARNING", + "--steeringFile=steering.py" + # #"npsim", + # "python3", "npsim_stepping.py" + # "--compactFile", detector_path, + # "-N", str(event_num), + # "--inputFiles", url, + # "--random.seed", "1", + # "--outputFile", output_file, + # "--steeringFile=steering.py" + ] + + # Run the simulation + run_command(npsim_command) + + # Command for converting the output file to JSON format + conversion_command = [ + "python3", + "dd4hep_txt_to_json.py", + "--event-prefix", event_prefix, + output_evttxt + ] + + # Run the conversion + run_command(conversion_command) + +setup_environment() + +# Set the detector path (assuming it's predefined as an environment variable or explicitly defined here) +DETECTOR_PATH = os.getenv('DETECTOR_PATH', '/opt/detector/epic-main/share/epic/') + +# Matrix definitions for beams and Q2 values +beams = ['5x41', '10x100', '18x275'] +minq2s = [1, 100, 1000] + +# Iterate over each combination of beam and minq2 +for beam in beams: + for minq2 in minq2s: + print("campaign file:") + print(get_reco_path('24.08.1', beam, minq2)) + run_simulation(beam, minq2, 10, '/opt/detector/epic-main/share/epic/epic_full.xml') From f0a2410410e0e9c5497815a24f8585f0a2da6aa2 Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Fri, 18 Oct 2024 20:45:56 -0400 Subject: [PATCH 06/14] Use environment variables for click and flask. Better CORS handling --- pyrobird/README.md | 4 +- pyrobird/example_wsgi.py | 4 +- pyrobird/pyproject.toml | 2 +- pyrobird/src/pyrobird/__about__.py | 2 +- pyrobird/src/pyrobird/cli/serve.py | 12 +++--- pyrobird/src/pyrobird/edm4eic.py | 2 +- pyrobird/src/pyrobird/server/__init__.py | 51 +++++++++++++----------- pyrobird/tests/test_server.py | 24 +++++------ 8 files changed, 53 insertions(+), 48 deletions(-) diff --git a/pyrobird/README.md b/pyrobird/README.md index ae93c8f..bd7bcfa 100644 --- a/pyrobird/README.md +++ b/pyrobird/README.md @@ -134,8 +134,8 @@ This is technical explanation of what is under the hood of the server part ### Configuration Options - **DOWNLOAD_PATH**: `str[getcwd()]`, Specifies the directory from which files can be downloaded when using relative paths. -- **DOWNLOAD_IS_DISABLED**: `bool[False]` If set to `True`, all download functionalities are disabled. -- **DOWNLOAD_IS_UNRESTRICTED**: `bool[False]`, allows unrestricted access to download any file, including sensitive ones. +- **PYROBIRD_DOWNLOAD_IS_DISABLED**: `bool[False]` If set to `True`, all download functionalities are disabled. +- **PYROBIRD_DOWNLOAD_IS_UNRESTRICTED**: `bool[False]`, allows unrestricted access to download any file, including sensitive ones. - **CORS_IS_ALLOWED**: `bool[False]`, If set to `True`, enables Cross-Origin Resource Sharing (CORS) for download routes. diff --git a/pyrobird/example_wsgi.py b/pyrobird/example_wsgi.py index 6d8b6ea..1a8ad1b 100644 --- a/pyrobird/example_wsgi.py +++ b/pyrobird/example_wsgi.py @@ -1,6 +1,6 @@ config = { - "DOWNLOAD_IS_DISABLED": True, - "DOWNLOAD_IS_UNRESTRICTED": False, + "PYROBIRD_DOWNLOAD_IS_DISABLED": True, + "PYROBIRD_DOWNLOAD_IS_UNRESTRICTED": False, "CORS_IS_ALLOWED": True } diff --git a/pyrobird/pyproject.toml b/pyrobird/pyproject.toml index 6b295d3..4c42007 100644 --- a/pyrobird/pyproject.toml +++ b/pyrobird/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ - "click", "rich", "pyyaml", "flask", "flask_cors", "flask-compress", "json5", "uproot" + "click", "rich", "pyyaml", "flask", "flask-cors", "flask-compress", "json5", "uproot" ] [project.optional-dependencies] diff --git a/pyrobird/src/pyrobird/__about__.py b/pyrobird/src/pyrobird/__about__.py index 62015d7..26507a3 100644 --- a/pyrobird/src/pyrobird/__about__.py +++ b/pyrobird/src/pyrobird/__about__.py @@ -2,4 +2,4 @@ # This file is part of Firebird Event Display and is licensed under the LGPLv3. # See the LICENSE file in the project root for full license information. -__version__ = "0.1.19" +__version__ = "0.1.23" diff --git a/pyrobird/src/pyrobird/cli/serve.py b/pyrobird/src/pyrobird/cli/serve.py index 1181d3e..0c5077a 100644 --- a/pyrobird/src/pyrobird/cli/serve.py +++ b/pyrobird/src/pyrobird/cli/serve.py @@ -22,14 +22,14 @@ @click.command() -@click.option("--allow-any-file", "unsecure_files", is_flag=True, show_default=True, default=False, help=unsecure_files_help) -@click.option("--allow-cors", "allow_cors", is_flag=True, show_default=True, default=False, help=allow_cors_help) -@click.option("--disable-files", "disable_download", is_flag=True, show_default=True, default=False, help="Disable all file downloads from the server") -@click.option("--work-path", "work_path", show_default=True, default="", help="Set the base directory path for file downloads. Defaults to the current working directory.") +@click.option("--allow-any-file", "unsecure_files", envvar=CFG_DOWNLOAD_IS_UNRESTRICTED, is_flag=True, show_default=True, default=False, help=unsecure_files_help) +@click.option("--allow-cors", "allow_cors", envvar=CFG_CORS_IS_ALLOWED, is_flag=True, show_default=True, default=False, help=allow_cors_help) +@click.option("--disable-files", "disable_download", envvar=CFG_DOWNLOAD_IS_DISABLED, is_flag=True, show_default=True, default=False, help="Disable all file downloads from the server") +@click.option("--work-path", "work_path", envvar=CFG_DOWNLOAD_PATH, show_default=True, default="", help="Set the base directory path for file downloads. Defaults to the current working directory.") @click.option("--host", "host", default="", help="Set the host for development server to listen to") @click.option("--port", "port", default="", help="Set the port for development server to listen to") -@click.option("--api-url", "api_url", default="", help="Force to use this address as backend API base URL. E.g. https://my-server:1234/") -@click.option("--config", "config_path", default="", help="Path to firebird config.jsonc if used a custom") +@click.option("--api-url", "api_url", envvar=CFG_API_BASE_URL, default="", help="Force to use this address as backend API base URL. E.g. https://my-server:1234/") +@click.option("--config", "config_path", envvar=CFG_FIREBIRD_CONFIG_PATH, default="", help="Path to firebird config.jsonc if used a custom") @click.pass_context def serve(ctx, unsecure_files, allow_cors, disable_download, work_path, host, port, api_url, config_path): """ diff --git a/pyrobird/src/pyrobird/edm4eic.py b/pyrobird/src/pyrobird/edm4eic.py index ae838dc..db5a8fa 100644 --- a/pyrobird/src/pyrobird/edm4eic.py +++ b/pyrobird/src/pyrobird/edm4eic.py @@ -156,7 +156,7 @@ def get_field_array(field_branch): group = { "name": branch_name, - "type": "HitBox", + "type": "BoxTrackerHit", "originType": "edm4eic::TrackerHitData", "hits": hits, } diff --git a/pyrobird/src/pyrobird/server/__init__.py b/pyrobird/src/pyrobird/server/__init__.py index 078489d..73c8403 100644 --- a/pyrobird/src/pyrobird/server/__init__.py +++ b/pyrobird/src/pyrobird/server/__init__.py @@ -37,13 +37,15 @@ # Config KEYS -CFG_DOWNLOAD_IS_UNRESTRICTED = "DOWNLOAD_IS_UNRESTRICTED" -CFG_DOWNLOAD_IS_DISABLED = "DOWNLOAD_IS_DISABLED" -CFG_DOWNLOAD_PATH = "DOWNLOAD_PATH" -CFG_CORS_IS_ALLOWED = "DOWNLOAD_ALLOW_CORS" -CFG_API_BASE_URL = "API_BASE_URL" -CFG_FIREBIRD_CONFIG_PATH = "FIREBIRD_CONFIG_PATH" +CFG_DOWNLOAD_IS_UNRESTRICTED = "PYROBIRD_DOWNLOAD_IS_UNRESTRICTED" +CFG_DOWNLOAD_IS_DISABLED = "PYROBIRD_DOWNLOAD_IS_DISABLED" +CFG_DOWNLOAD_PATH = "PYROBIRD_DOWNLOAD_PATH" +CFG_CORS_IS_ALLOWED = "PYROBIRD_CORS_IS_ALLOWED" +CFG_API_BASE_URL = "PYROBIRD_API_BASE_URL" +CFG_FIREBIRD_CONFIG_PATH = "PYROBIRD_FIREBIRD_CONFIG_PATH" +# Get +flask_app.config[CFG_CORS_IS_ALLOWED] = str(os.environ.get(CFG_CORS_IS_ALLOWED, '')).lower() in ('1', 'true') class ExcludeAPIConverter(BaseConverter): @@ -90,10 +92,10 @@ def _can_user_download_file(filename): - bool: True if the file can be downloaded, False otherwise. Process: - - If downloading is globally disabled (DOWNLOAD_IS_DISABLED=True), returns False. - - If unrestricted downloads are allowed (DOWNLOAD_IS_UNRESTRICTED=True), returns True. + - If downloading is globally disabled (PYROBIRD_DOWNLOAD_IS_DISABLED=True), returns False. + - If unrestricted downloads are allowed (PYROBIRD_DOWNLOAD_IS_UNRESTRICTED=True), returns True. - For relative paths, assumes that the download is allowable. - - For absolute paths, checks that the file resides within the configured DOWNLOAD_PATH. + - For absolute paths, checks that the file resides within the configured PYROBIRD_DOWNLOAD_PATH. - Logs a warning and returns False if the file is outside the allowed download path or if downloading is disabled. """ @@ -101,7 +103,7 @@ def _can_user_download_file(filename): # If any downloads are disabled if app.config.get(CFG_DOWNLOAD_IS_DISABLED) is True: - logger.warning("Can't download file. DOWNLOAD_IS_DISABLED=True") + logger.warning("Can't download file. PYROBIRD_DOWNLOAD_IS_DISABLED=True") return False # If we allow any download @@ -110,7 +112,7 @@ def _can_user_download_file(filename): return True # if allowed/disable checks are done, and we are here, - # if relative path is given, it will be joined with DOWNLOAD_PATH + # if relative path is given, it will be joined with PYROBIRD_DOWNLOAD_PATH if not os.path.isabs(filename): return True @@ -123,7 +125,7 @@ def _can_user_download_file(filename): # Check file will be downloaded from safe folder can_download = os.path.realpath(filename).startswith(os.path.realpath(allowed_path)) if not can_download: - logger.warning("Can't download file. File is not in DOWNLOAD_PATH") + logger.warning("Can't download file. File is not in PYROBIRD_DOWNLOAD_PATH") return False # All is fine! @@ -146,7 +148,7 @@ def download_file(filename=None): if not _can_user_download_file(filename): abort(404) - # If it is relative, combine it with DOWNLOAD_PATH + # If it is relative, combine it with PYROBIRD_DOWNLOAD_PATH if not os.path.isabs(filename): download_path = flask.current_app.config.get(CFG_DOWNLOAD_PATH) if not download_path: @@ -358,20 +360,23 @@ def shutdown(): def run(config=None, host=None, port=5454, debug=False, load_dotenv=False): """Runs flask server""" if config: - if isinstance(config, flask.Config) or isinstance(config, map): + if isinstance(config, flask.Config) or isinstance(config, map) or isinstance(config, dict): flask_app.config.from_mapping(config) else: flask_app.config.from_object(config) - if flask_app.config and flask_app.config.get(CFG_CORS_IS_ALLOWED) is True: - from flask_cors import CORS - - # Enable CORS for all routes and specify the domains and settings - CORS(flask_app, resources={ - r"/download/*": {"origins": "*"}, - r"/api/v1/*": {"origins": "*"}, - r"/assets/config.jsonc": {"origins": "*"}, - }) + if flask_app.config: + cfg_cors_allowed = flask_app.config.get(CFG_CORS_IS_ALLOWED) + if cfg_cors_allowed: + logger.info("flask_app.config.get(CFG_CORS_IS_ALLOWED) is True") + from flask_cors import CORS + + # Enable CORS for all routes and specify the domains and settings + CORS(flask_app, resources={ + r"/download/*": {"origins": "*"}, + r"/api/v1/*": {"origins": "*"}, + r"/assets/config.jsonc": {"origins": "*"}, + }) logger.debug("Serve path:") logger.debug(" Server dir :", server_dir) diff --git a/pyrobird/tests/test_server.py b/pyrobird/tests/test_server.py index 71a7123..3284110 100644 --- a/pyrobird/tests/test_server.py +++ b/pyrobird/tests/test_server.py @@ -19,11 +19,11 @@ def client(): # Configure the Flask app for testing flask_app.config['TESTING'] = True - # Set the DOWNLOAD_PATH to the 'data' directory where test files are located - flask_app.config['DOWNLOAD_PATH'] = os.path.abspath(TEST_ROOT_DATA_DIR) + # Set the PYROBIRD_DOWNLOAD_PATH to the 'data' directory where test files are located + flask_app.config['PYROBIRD_DOWNLOAD_PATH'] = os.path.abspath(TEST_ROOT_DATA_DIR) # Ensure downloads are allowed - flask_app.config['DOWNLOAD_IS_DISABLED'] = False - flask_app.config['DOWNLOAD_IS_UNRESTRICTED'] = False + flask_app.config['PYROBIRD_DOWNLOAD_IS_DISABLED'] = False + flask_app.config['PYROBIRD_DOWNLOAD_IS_UNRESTRICTED'] = False return flask_app.test_client() @@ -44,7 +44,7 @@ def test_open_edm4eic_file_local_allowed(client): def test_open_edm4eic_file_local_not_allowed(client): from urllib.parse import quote - # Test accessing a local file outside of DOWNLOAD_PATH + # Test accessing a local file outside of PYROBIRD_DOWNLOAD_PATH filename = '/etc/passwd' # A file outside the allowed path event_number = 0 encoded_filename = quote(filename, safe='') @@ -54,11 +54,11 @@ def test_open_edm4eic_file_local_not_allowed(client): def test_open_dangerous(client): - # Test accessing a local file outside of DOWNLOAD_PATH + # Test accessing a local file outside of PYROBIRD_DOWNLOAD_PATH filename = '/etc/passwd' # A file outside the allowed path event_number = 0 - flask_app.config['DOWNLOAD_IS_UNRESTRICTED'] = True - flask_app.config['DOWNLOAD_IS_DISABLED'] = False + flask_app.config['PYROBIRD_DOWNLOAD_IS_UNRESTRICTED'] = True + flask_app.config['PYROBIRD_DOWNLOAD_IS_DISABLED'] = False response = client.get(f'/api/v1/download?filename={filename}') assert response.status_code == 200 # OK @@ -81,9 +81,9 @@ def test_open_edm4eic_file_nonexistent_file(client): assert response.status_code == 404 # Not Found -def test_open_edm4eic_file_DOWNLOAD_IS_DISABLEDd(client): +def test_open_edm4eic_file_PYROBIRD_DOWNLOAD_IS_DISABLEDd(client): # Test accessing a file when downloads are disabled - flask_app.config['DOWNLOAD_IS_DISABLED'] = True + flask_app.config['PYROBIRD_DOWNLOAD_IS_DISABLED'] = True filename = 'reco_2024-09_craterlake_2evt.edm4eic.root' event_number = 0 @@ -92,14 +92,14 @@ def test_open_edm4eic_file_DOWNLOAD_IS_DISABLEDd(client): assert response.status_code == 403 # Forbidden # Re-enable downloads for other tests - flask_app.config['DOWNLOAD_IS_DISABLED'] = False + flask_app.config['PYROBIRD_DOWNLOAD_IS_DISABLED'] = False def test_open_edm4eic_file_invalid_file(client): # Test accessing a file that is not a valid ROOT file # Create an invalid file in the data directory invalid_filename = 'invalid_file.root' - invalid_file_path = os.path.join(flask_app.config['DOWNLOAD_PATH'], invalid_filename) + invalid_file_path = os.path.join(flask_app.config['PYROBIRD_DOWNLOAD_PATH'], invalid_filename) with open(invalid_file_path, 'w') as f: f.write('This is not a valid ROOT file.') From e7c83428fc7ac815c6c5e9c315c6771d8aa7de90 Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Fri, 18 Oct 2024 20:46:45 -0400 Subject: [PATCH 07/14] Correct work with edm4eic files from remote API --- firebird-ng/package-lock.json | 4 +- firebird-ng/package.json | 4 +- .../nav-config/nav-config.component.html | 19 ++- .../nav-config/nav-config.component.scss | 8 ++ .../nav-config/nav-config.component.ts | 19 ++- .../input-config/input-config.component.html | 24 +--- .../main-display/main-display.component.ts | 29 ++++- .../box-tracker-hit-simple.painter.ts | 119 ++++++++++++++++++ .../src/app/painters/component-painter.ts | 6 +- .../src/app/painters/data-model-painter.ts | 10 +- .../src/app/services/data-model.service.ts | 10 +- .../src/app/utils/data-fetching.utils.ts | 6 +- .../src/assets/icons/github-mark-white.svg | 2 + firebird-ng/src/assets/icons/github-mark.svg | 2 + firebird-ng/tsconfig.json | 1 + 15 files changed, 224 insertions(+), 39 deletions(-) create mode 100644 firebird-ng/src/app/painters/box-tracker-hit-simple.painter.ts create mode 100644 firebird-ng/src/assets/icons/github-mark-white.svg create mode 100644 firebird-ng/src/assets/icons/github-mark.svg diff --git a/firebird-ng/package-lock.json b/firebird-ng/package-lock.json index 26797a0..8c756bb 100644 --- a/firebird-ng/package-lock.json +++ b/firebird-ng/package-lock.json @@ -1,12 +1,12 @@ { "name": "firebird", - "version": "0.0.5", + "version": "0.1.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "firebird", - "version": "0.0.5", + "version": "0.1.20", "dependencies": { "@angular/animations": "^17.3.12", "@angular/cdk": "~17.3.10", diff --git a/firebird-ng/package.json b/firebird-ng/package.json index be2afaa..8d5f76c 100644 --- a/firebird-ng/package.json +++ b/firebird-ng/package.json @@ -1,6 +1,6 @@ { "name": "firebird", - "version": "0.0.5", + "version": "0.1.23", "scripts": { "ng": "ng", "serve": "ng serve", @@ -11,7 +11,7 @@ "test": "ng test", "test:headless": "ng test --browsers=ChromeHeadless --watch=false --code-coverage --progress=false" }, - "private": true, + "private": false, "dependencies": { "@angular/animations": "^17.3.12", "@angular/cdk": "~17.3.10", diff --git a/firebird-ng/src/app/components/nav-config/nav-config.component.html b/firebird-ng/src/app/components/nav-config/nav-config.component.html index c18d53b..7971d71 100644 --- a/firebird-ng/src/app/components/nav-config/nav-config.component.html +++ b/firebird-ng/src/app/components/nav-config/nav-config.component.html @@ -2,10 +2,25 @@ {{ isNavConfigOpen ? 'close' : 'menu' }} - + diff --git a/firebird-ng/src/app/components/nav-config/nav-config.component.scss b/firebird-ng/src/app/components/nav-config/nav-config.component.scss index 7bbcf35..a42d94d 100644 --- a/firebird-ng/src/app/components/nav-config/nav-config.component.scss +++ b/firebird-ng/src/app/components/nav-config/nav-config.component.scss @@ -16,3 +16,11 @@ top: 0; padding-top: -3px; } + +.logo-button { + background: none; + border: none; + padding: 0; + margin: 0; + cursor: pointer; +} diff --git a/firebird-ng/src/app/components/nav-config/nav-config.component.ts b/firebird-ng/src/app/components/nav-config/nav-config.component.ts index ffcf020..0a553a9 100644 --- a/firebird-ng/src/app/components/nav-config/nav-config.component.ts +++ b/firebird-ng/src/app/components/nav-config/nav-config.component.ts @@ -4,6 +4,10 @@ import {MatIcon} from "@angular/material/icon"; import {NgIf} from "@angular/common"; import {MatIconButton} from "@angular/material/button"; import {MatTooltip} from "@angular/material/tooltip"; +import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu"; +import { MatIconRegistry } from '@angular/material/icon'; +import { DomSanitizer } from '@angular/platform-browser'; +import packageInfo from '../../../../package.json'; @Component({ selector: 'app-nav-config', @@ -14,7 +18,10 @@ import {MatTooltip} from "@angular/material/tooltip"; MatIcon, NgIf, MatIconButton, - MatTooltip + MatTooltip, + MatMenu, + MatMenuItem, + MatMenuTrigger ], templateUrl: './nav-config.component.html', styleUrl: './nav-config.component.scss' @@ -22,6 +29,16 @@ import {MatTooltip} from "@angular/material/tooltip"; export class NavConfigComponent { isNavConfigOpen: boolean = false; isSmallScreen: boolean = window.innerWidth < 992; + packageVersion: string; + + constructor(private matIconRegistry: MatIconRegistry, + private domSanitizer: DomSanitizer) { + this.matIconRegistry.addSvgIcon( + 'github', + this.domSanitizer.bypassSecurityTrustResourceUrl('assets/icons/github-mark-white.svg') + ); + this.packageVersion = packageInfo.version; + } @HostListener('window:resize', ['$event']) onResize(event: any) { diff --git a/firebird-ng/src/app/pages/input-config/input-config.component.html b/firebird-ng/src/app/pages/input-config/input-config.component.html index db310ae..a7af0b0 100644 --- a/firebird-ng/src/app/pages/input-config/input-config.component.html +++ b/firebird-ng/src/app/pages/input-config/input-config.component.html @@ -3,9 +3,10 @@

Configure geometry pipeline

- geometry-pipeline + + geometry-pipeline -
+
@@ -85,11 +86,11 @@
Merge geometries if possible
Server API Configuration
- +
-
API URL:
+
Base API URL:
@@ -98,21 +99,6 @@
API URL:
- - - - - - - - - - - - - - -
diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.ts b/firebird-ng/src/app/pages/main-display/main-display.component.ts index e8c4485..a444f2a 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.ts +++ b/firebird-ng/src/app/pages/main-display/main-display.component.ts @@ -49,6 +49,7 @@ import {DataModelPainter} from "../../painters/data-model-painter"; import {AppComponent} from "../../app.component"; import {ToolPanelComponent} from "../../components/tool-panel/tool-panel.component"; import {NavConfigComponent} from "../../components/nav-config/nav-config.component"; +import {UrlService} from "../../services/url.service"; // import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; @@ -57,7 +58,7 @@ import {NavConfigComponent} from "../../components/nav-config/nav-config.compone @Component({ selector: 'app-test-experiment', templateUrl: './main-display.component.html', - imports: [PhoenixUIModule, IoOptionsComponent, MatSlider, MatIcon, MatButton, MatSliderThumb, DecimalPipe, MatTooltip, MatFormField, MatSelect, MatOption, NgForOf, AngularSplitModule, SceneTreeComponent, NgClass, MatIconButton, DisplayShellComponent, AppComponent, RouterOutlet, RouterLink, ToolPanelComponent, NavConfigComponent, NgIf], + imports: [PhoenixUIModule, IoOptionsComponent, MatSlider, MatIcon, MatButton, MatSliderThumb, DecimalPipe, MatTooltip, MatFormField, MatSelect, MatOption, NgForOf, AngularSplitModule, SceneTreeComponent, NgClass, MatIconButton, DisplayShellComponent, ToolPanelComponent, NavConfigComponent, NgIf], standalone: true, styleUrls: ['./main-display.component.scss'] }) @@ -121,7 +122,9 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { private route: ActivatedRoute, private settings: UserConfigService, private dataService: DataModelService, - private elRef: ElementRef, private renderer2: Renderer2, + private elRef: ElementRef, + private renderer2: Renderer2, + private urlService: UrlService, private _snackBar: MatSnackBar) { this.threeFacade = new PhoenixThreeFacade(this.eventDisplay); } @@ -443,6 +446,7 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { ngOnInit() { let eventSource = this.settings.trajectoryEventSource.value; + eventSource = this.urlService.resolveDownloadUrl(eventSource); let eventConfig = {eventFile: "https://firebird-eic.org/py8_all_dis-cc_beam-5x41_minq2-100_nevt-5.evt.json.zip", eventType: "zip"}; if( eventSource != "no-events" && !eventSource.endsWith("edm4hep.json")) { let eventType = eventSource.endsWith("zip") ? "zip" : "json"; @@ -641,10 +645,24 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { this.dataService.loadEdm4EicData().then(data => { this.updateSceneTreeComponent(); - console.log("loaded data model"); + console.log("loadEdm4EicData data:"); console.log(data); - }) + if(data == null) { + console.warn("DataService.loadEdm4EicData() Received data is null or undefined"); + return; + } + if(data.entries?.length ?? 0 > 0) { + this.painter.setThreeSceneParent(openThreeManager.sceneManager.getEventData()); + this.painter.setEntry(data.entries[0]); + this.painter.paint(this.currentTime); + this.updateSceneTreeComponent(); + } else { + console.warn("DataService.loadEdm4EicData() Received data had no entries"); + console.log(data); + } + }) + // this.dataService.loadDexData().then(data => { this.updateSceneTreeComponent(); if(data == null) { @@ -653,8 +671,10 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { } if(data.entries?.length ?? 0 > 0) { + this.painter.setThreeSceneParent(openThreeManager.sceneManager.getEventData()); this.painter.setEntry(data.entries[0]); this.painter.paint(this.currentTime); + this.updateSceneTreeComponent(); } else { console.warn("DataService.loadDexData() Received data had no entries"); console.log(data); @@ -878,6 +898,7 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { exitTimedDisplay() { this.stopAnimation(); this.rewindTime(); + this.painter.paint(null); this.animateEventAfterLoad = false; if(this.trackInfos) { for (let trackInfo of this.trackInfos) { diff --git a/firebird-ng/src/app/painters/box-tracker-hit-simple.painter.ts b/firebird-ng/src/app/painters/box-tracker-hit-simple.painter.ts new file mode 100644 index 0000000..b8423df --- /dev/null +++ b/firebird-ng/src/app/painters/box-tracker-hit-simple.painter.ts @@ -0,0 +1,119 @@ +import { + Object3D, + Mesh, + BoxGeometry, + MeshBasicMaterial, + Color, +} from 'three'; +import { ComponentPainter } from './component-painter'; +import { BoxTrackerHitComponent } from '../model/box-tracker-hit.component'; +import { EntryComponent } from '../model/entry-component'; + +/** + * Alternative Painter class for rendering BoxTrackerHitComponent using individual Meshes. + */ +export class BoxTrackerHitSimplePainter extends ComponentPainter { + /** Array of Mesh objects representing hits */ + private hitMeshes: Mesh[] = []; + + private boxComponent: BoxTrackerHitComponent; + + /** + * Constructs a new BoxTrackerHitAlternativePainter. + * + * @param parentNode - The Object3D node where the hit meshes will be added. + * @param component - The BoxTrackerHitComponent containing the hit data. + */ + constructor(parentNode: Object3D, component: EntryComponent) { + super(parentNode, component); + + // Runtime type check + if (component.type !== BoxTrackerHitComponent.type) { + throw new Error('Invalid component type for BoxTrackerHitAlternativePainter'); + } + + this.boxComponent = component as BoxTrackerHitComponent; + + // Create a bright random color for this component collection + const hue = Math.random(); + const randomColor = new Color().setHSL(hue, 1, 0.5); // Bright color + + // Create a material with the random color + const material = new MeshBasicMaterial({ color: randomColor }); + + // Create a mesh for each hit using the same material + this.createHitMeshes(material); + } + + /** + * Creates Mesh instances for each hit and adds them to the parent node. + * + * @param material - The material to use for the hit meshes. + */ + private createHitMeshes(material: MeshBasicMaterial): void { + for (const hit of this.boxComponent.hits) { + // Create geometry for the box + const geometry = new BoxGeometry(10,10,10 + // hit.dimensions[0], + // hit.dimensions[1], + // hit.dimensions[2] + ); + + // Create the mesh + const mesh = new Mesh(geometry, material); + + // Set position + mesh.position.set(hit.position[0], hit.position[1], hit.position[2]); + + // Store the hit time + mesh.userData['appearanceTime'] = hit.time[0]; + + // Initially make the mesh invisible + mesh.visible = false; + + // Add the mesh to the parent node and to the array + this.parentNode.add(mesh); + this.hitMeshes.push(mesh); + } + } + + /** + * Paint method to update the visibility of the hits based on time. + * + * @param time - The current time in nanoseconds or null for static rendering. + */ + public paint(time: number | null): void { + for (const mesh of this.hitMeshes) { + if (time !== null) { + // Show the mesh if its appearance time is less than or equal to the current time + mesh.visible = mesh.userData['appearanceTime'] <= time; + } else { + // In static mode, make all meshes visible + mesh.visible = true; + } + } + } + + /** + * Dispose of resources used by the painter. + */ + override dispose(): void { + for (const mesh of this.hitMeshes) { + // Dispose of geometry and material + mesh.geometry.dispose(); + + // Dispose of the material only if it's not shared with other meshes + if (mesh.material instanceof MeshBasicMaterial) { + mesh.material.dispose(); + } + + // Remove the mesh from the parent node + this.parentNode.remove(mesh); + } + + // Clear the array + this.hitMeshes = []; + + super.dispose(); + } +} diff --git a/firebird-ng/src/app/painters/component-painter.ts b/firebird-ng/src/app/painters/component-painter.ts index c1f6574..e30d0c5 100644 --- a/firebird-ng/src/app/painters/component-painter.ts +++ b/firebird-ng/src/app/painters/component-painter.ts @@ -10,7 +10,7 @@ export type ComponentPainterConstructor = new (node: Object3D, component: EntryC export abstract class ComponentPainter { /** Constructor is public since we can instantiate directly */ - constructor(protected node: Object3D, protected component: EntryComponent) {} + constructor(protected parentNode: Object3D, protected component: EntryComponent) {} /** Gets the `type` identifier for the component this class works with */ public get componentType() { @@ -26,8 +26,8 @@ export abstract class ComponentPainter { /** Dispose method to clean up resources */ public dispose(): void { // Remove node from the scene - if (this.node) { - disposeNode(this.node); + if (this.parentNode) { + disposeNode(this.parentNode); } } } diff --git a/firebird-ng/src/app/painters/data-model-painter.ts b/firebird-ng/src/app/painters/data-model-painter.ts index f498525..5620f68 100644 --- a/firebird-ng/src/app/painters/data-model-painter.ts +++ b/firebird-ng/src/app/painters/data-model-painter.ts @@ -1,8 +1,15 @@ +/** + * This class is responsible in rendering Event or Frame data. + * It first takes event components and manipulates three.js Scene + * Then responsible for correct rendering at a given time + */ + import { Entry } from "../model/entry"; import { Object3D, Group } from "three"; import {ComponentPainter, ComponentPainterConstructor} from "./component-painter"; import {BoxTrackerHitComponent} from "../model/box-tracker-hit.component"; import {BoxTrackerHitPainter} from "./box-tracker-hit.painter"; +import {BoxTrackerHitSimplePainter} from "./box-tracker-hit-simple.painter"; export class DataModelPainter { @@ -14,7 +21,8 @@ export class DataModelPainter { public constructor() { // Register builtin painters - this.registerPainter(BoxTrackerHitComponent.type, BoxTrackerHitPainter); + //this.registerPainter(BoxTrackerHitComponent.type, BoxTrackerHitPainter); + this.registerPainter(BoxTrackerHitComponent.type, BoxTrackerHitSimplePainter); } diff --git a/firebird-ng/src/app/services/data-model.service.ts b/firebird-ng/src/app/services/data-model.service.ts index a2fded2..767b7fe 100644 --- a/firebird-ng/src/app/services/data-model.service.ts +++ b/firebird-ng/src/app/services/data-model.service.ts @@ -7,7 +7,7 @@ import {HttpClient} from "@angular/common/http"; import {firstValueFrom} from "rxjs"; import {UrlService} from "./url.service"; import {DataExchange} from "../model/data-exchange"; -import {loadJSONFileEvents, loadZipFileEvents} from "../utils/data-fetching.utils"; +import {fetchTextFile, loadJSONFileEvents, loadZipFileEvents} from "../utils/data-fetching.utils"; @Injectable({ providedIn: 'root' @@ -50,9 +50,11 @@ export class DataModelService { // userInput = this.urlService.resolveLocalhostUrl(userInput); // } - const jsonData = await firstValueFrom( - this.http.get(url, { responseType: 'text' }) - ); + const jsonData = await fetchTextFile(url); + // //this.http.get(url, { responseType: 'text' }) + // ); + + const dexData = JSON.parse(jsonData); let data = DataExchange.fromDexObj(dexData); diff --git a/firebird-ng/src/app/utils/data-fetching.utils.ts b/firebird-ng/src/app/utils/data-fetching.utils.ts index b172892..e65b0cc 100644 --- a/firebird-ng/src/app/utils/data-fetching.utils.ts +++ b/firebird-ng/src/app/utils/data-fetching.utils.ts @@ -28,7 +28,11 @@ export async function fetchTextFile(fileURL: string): Promise { try{ const loadingTimeMessage = `${fetchTextFile.name}: fetching ${fileURL}`; console.time(loadingTimeMessage); - const fileText = await (await fetch(fileURL)).text(); + const response = await fetch(fileURL); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const fileText = await response.text(); console.timeEnd(loadingTimeMessage); return fileText; } diff --git a/firebird-ng/src/assets/icons/github-mark-white.svg b/firebird-ng/src/assets/icons/github-mark-white.svg new file mode 100644 index 0000000..e294208 --- /dev/null +++ b/firebird-ng/src/assets/icons/github-mark-white.svg @@ -0,0 +1,2 @@ + + diff --git a/firebird-ng/src/assets/icons/github-mark.svg b/firebird-ng/src/assets/icons/github-mark.svg new file mode 100644 index 0000000..e6b695c --- /dev/null +++ b/firebird-ng/src/assets/icons/github-mark.svg @@ -0,0 +1,2 @@ + + diff --git a/firebird-ng/tsconfig.json b/firebird-ng/tsconfig.json index c018c49..46af714 100644 --- a/firebird-ng/tsconfig.json +++ b/firebird-ng/tsconfig.json @@ -18,6 +18,7 @@ "target": "ES2022", "module": "ES2022", "useDefineForClassFields": false, + "resolveJsonModule": true, "lib": [ "ES2022", "dom" From ee619b2803e57647a4cae348647430b5fdaa7452 Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Sun, 20 Oct 2024 22:22:06 -0400 Subject: [PATCH 08/14] Split flask run into configure and run --- pyrobird/example_wsgi.py | 2 +- pyrobird/src/pyrobird/server/__init__.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyrobird/example_wsgi.py b/pyrobird/example_wsgi.py index 1a8ad1b..8344a03 100644 --- a/pyrobird/example_wsgi.py +++ b/pyrobird/example_wsgi.py @@ -1,7 +1,7 @@ config = { "PYROBIRD_DOWNLOAD_IS_DISABLED": True, "PYROBIRD_DOWNLOAD_IS_UNRESTRICTED": False, - "CORS_IS_ALLOWED": True + "PYROBIRD_CORS_IS_ALLOWED": True } from pyrobird.server import flask_app as application diff --git a/pyrobird/src/pyrobird/server/__init__.py b/pyrobird/src/pyrobird/server/__init__.py index 73c8403..12ae703 100644 --- a/pyrobird/src/pyrobird/server/__init__.py +++ b/pyrobird/src/pyrobird/server/__init__.py @@ -357,8 +357,8 @@ def shutdown(): return 'Server shutting down...' -def run(config=None, host=None, port=5454, debug=False, load_dotenv=False): - """Runs flask server""" +def configure_flask_app(config=None): + """Returns""" if config: if isinstance(config, flask.Config) or isinstance(config, map) or isinstance(config, dict): flask_app.config.from_mapping(config) @@ -381,5 +381,9 @@ def run(config=None, host=None, port=5454, debug=False, load_dotenv=False): logger.debug("Serve path:") logger.debug(" Server dir :", server_dir) logger.debug(" Static dir :", static_dir) + return flask_app + +def run(config=None, host=None, port=5454, debug=False, load_dotenv=False): + configure_flask_app(config) flask_app.run(host=host, port=port, debug=debug, load_dotenv=load_dotenv) From 35c793ef34ae44549f92ab08c9a7d23ad49af954 Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Tue, 29 Oct 2024 01:25:38 +0200 Subject: [PATCH 09/14] Everything works without phoenix except dark theme --- firebird-ng/package-lock.json | 4 +- .../auto-rotate/auto-rotate.component.html | 7 + .../auto-rotate/auto-rotate.component.scss | 0 .../auto-rotate/auto-rotate.component.test.ts | 49 ++++ .../auto-rotate/auto-rotate.component.ts | 23 ++ .../dark-theme/dark-theme.component.html | 7 + .../dark-theme/dark-theme.component.scss | 0 .../dark-theme/dark-theme.component.test.ts | 51 ++++ .../dark-theme/dark-theme.component.ts | 27 ++ .../event-selector.component.html | 12 + .../event-selector.component.scss | 11 + .../event-selector.component.test.ts | 56 ++++ .../event-selector.component.ts | 34 +++ .../main-view-toggle.component.html | 8 + .../main-view-toggle.component.scss | 0 .../main-view-toggle.component.test.ts | 49 ++++ .../main-view-toggle.component.ts | 28 ++ .../menu-toggle/menu-toggle.component.html | 11 + .../menu-toggle/menu-toggle.component.scss | 38 +++ .../menu-toggle/menu-toggle.component.test.ts | 25 ++ .../menu-toggle/menu-toggle.component.ts | 20 ++ .../object-clipping.component.html | 52 ++++ .../object-clipping.component.scss | 8 + .../object-clipping.component.test.ts | 84 ++++++ .../object-clipping.component.ts | 54 ++++ .../tool-panel/tool-panel.component.html | 4 +- .../tool-panel/tool-panel.component.ts | 6 +- .../cartesian-grid-config.component.html | 157 ++++++++++ .../cartesian-grid-config.component.scss | 38 +++ .../cartesian-grid-config.component.test.ts | 269 ++++++++++++++++++ .../cartesian-grid-config.component.ts | 179 ++++++++++++ .../view-options/view-options.component.html | 103 +++++++ .../view-options/view-options.component.scss | 31 ++ .../view-options.component.test.ts | 193 +++++++++++++ .../view-options/view-options.component.ts | 108 +++++++ .../main-display/main-display.component.html | 24 +- .../main-display/main-display.component.ts | 9 +- 37 files changed, 1759 insertions(+), 20 deletions(-) create mode 100644 firebird-ng/src/app/components/auto-rotate/auto-rotate.component.html create mode 100644 firebird-ng/src/app/components/auto-rotate/auto-rotate.component.scss create mode 100644 firebird-ng/src/app/components/auto-rotate/auto-rotate.component.test.ts create mode 100644 firebird-ng/src/app/components/auto-rotate/auto-rotate.component.ts create mode 100644 firebird-ng/src/app/components/dark-theme/dark-theme.component.html create mode 100644 firebird-ng/src/app/components/dark-theme/dark-theme.component.scss create mode 100644 firebird-ng/src/app/components/dark-theme/dark-theme.component.test.ts create mode 100644 firebird-ng/src/app/components/dark-theme/dark-theme.component.ts create mode 100644 firebird-ng/src/app/components/event-selector/event-selector.component.html create mode 100644 firebird-ng/src/app/components/event-selector/event-selector.component.scss create mode 100644 firebird-ng/src/app/components/event-selector/event-selector.component.test.ts create mode 100644 firebird-ng/src/app/components/event-selector/event-selector.component.ts create mode 100644 firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.html create mode 100644 firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.scss create mode 100644 firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.test.ts create mode 100644 firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.ts create mode 100644 firebird-ng/src/app/components/menu-toggle/menu-toggle.component.html create mode 100644 firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss create mode 100644 firebird-ng/src/app/components/menu-toggle/menu-toggle.component.test.ts create mode 100644 firebird-ng/src/app/components/menu-toggle/menu-toggle.component.ts create mode 100644 firebird-ng/src/app/components/object-clipping/object-clipping.component.html create mode 100644 firebird-ng/src/app/components/object-clipping/object-clipping.component.scss create mode 100644 firebird-ng/src/app/components/object-clipping/object-clipping.component.test.ts create mode 100644 firebird-ng/src/app/components/object-clipping/object-clipping.component.ts create mode 100644 firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.html create mode 100644 firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.scss create mode 100644 firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.test.ts create mode 100644 firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.ts create mode 100644 firebird-ng/src/app/components/view-options/view-options.component.html create mode 100644 firebird-ng/src/app/components/view-options/view-options.component.scss create mode 100644 firebird-ng/src/app/components/view-options/view-options.component.test.ts create mode 100644 firebird-ng/src/app/components/view-options/view-options.component.ts diff --git a/firebird-ng/package-lock.json b/firebird-ng/package-lock.json index 8c756bb..cc13e2e 100644 --- a/firebird-ng/package-lock.json +++ b/firebird-ng/package-lock.json @@ -1,12 +1,12 @@ { "name": "firebird", - "version": "0.1.20", + "version": "0.1.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "firebird", - "version": "0.1.20", + "version": "0.1.23", "dependencies": { "@angular/animations": "^17.3.12", "@angular/cdk": "~17.3.10", diff --git a/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.html b/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.html new file mode 100644 index 0000000..cb3eb95 --- /dev/null +++ b/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.html @@ -0,0 +1,7 @@ + + diff --git a/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.scss b/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.test.ts b/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.test.ts new file mode 100644 index 0000000..8a6a94a --- /dev/null +++ b/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.test.ts @@ -0,0 +1,49 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AutoRotateComponent } from './auto-rotate.component'; +import { EventDisplayService } from '../../../services/event-display.service'; +import { PhoenixUIModule } from '../../phoenix-ui.module'; + +describe('AutoRotateComponent', () => { + let component: AutoRotateComponent; + let fixture: ComponentFixture; + + const mockEventDisplay = { + getUIManager: jest.fn().mockReturnThis(), + setAutoRotate: jest.fn().mockReturnThis(), + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PhoenixUIModule], + providers: [ + { + provide: EventDisplayService, + useValue: mockEventDisplay, + }, + ], + declarations: [AutoRotateComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AutoRotateComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should toggle auto rotate', () => { + expect(component.autoRotate).toBe(false); + + component.toggleAutoRotate(); + + expect(component.autoRotate).toBe(true); + expect(mockEventDisplay.getUIManager().setAutoRotate).toHaveBeenCalledWith( + component.autoRotate, + ); + }); +}); diff --git a/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.ts b/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.ts new file mode 100644 index 0000000..00686d2 --- /dev/null +++ b/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import {EventDisplayService} from "phoenix-ui-components"; +import {MenuToggleComponent} from "../menu-toggle/menu-toggle.component"; + +@Component({ + selector: 'app-custom-auto-rotate', + templateUrl: './auto-rotate.component.html', + styleUrls: ['./auto-rotate.component.scss'], + imports: [ + MenuToggleComponent + ], + standalone: true +}) +export class AutoRotateComponent { + autoRotate = false; + + constructor(private eventDisplay: EventDisplayService) {} + + toggleAutoRotate() { + this.autoRotate = !this.autoRotate; + this.eventDisplay.getUIManager().setAutoRotate(this.autoRotate); + } +} diff --git a/firebird-ng/src/app/components/dark-theme/dark-theme.component.html b/firebird-ng/src/app/components/dark-theme/dark-theme.component.html new file mode 100644 index 0000000..5ab48a9 --- /dev/null +++ b/firebird-ng/src/app/components/dark-theme/dark-theme.component.html @@ -0,0 +1,7 @@ + + diff --git a/firebird-ng/src/app/components/dark-theme/dark-theme.component.scss b/firebird-ng/src/app/components/dark-theme/dark-theme.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/firebird-ng/src/app/components/dark-theme/dark-theme.component.test.ts b/firebird-ng/src/app/components/dark-theme/dark-theme.component.test.ts new file mode 100644 index 0000000..2e3187c --- /dev/null +++ b/firebird-ng/src/app/components/dark-theme/dark-theme.component.test.ts @@ -0,0 +1,51 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DarkThemeComponent } from './dark-theme.component'; +import { EventDisplayService } from '../../../services/event-display.service'; +import { PhoenixUIModule } from '../../phoenix-ui.module'; + +describe('DarkThemeComponent', () => { + let component: DarkThemeComponent; + let fixture: ComponentFixture; + + const mockEventDisplay = { + getUIManager: jest.fn().mockReturnThis(), + getDarkTheme: jest.fn().mockReturnThis(), + setDarkTheme: jest.fn().mockReturnThis(), + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PhoenixUIModule], + providers: [ + { + provide: EventDisplayService, + useValue: mockEventDisplay, + }, + ], + declarations: [DarkThemeComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DarkThemeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initially get dark theme', () => { + jest.spyOn(mockEventDisplay, 'getDarkTheme'); + component.ngOnInit(); + expect(mockEventDisplay.getDarkTheme).toHaveBeenCalled(); + }); + + it('should set/toggle dark theme', () => { + component.darkTheme = false; + component.setDarkTheme(); + expect(component.darkTheme).toBe(true); + }); +}); diff --git a/firebird-ng/src/app/components/dark-theme/dark-theme.component.ts b/firebird-ng/src/app/components/dark-theme/dark-theme.component.ts new file mode 100644 index 0000000..051984e --- /dev/null +++ b/firebird-ng/src/app/components/dark-theme/dark-theme.component.ts @@ -0,0 +1,27 @@ +import { Component, type OnInit } from '@angular/core'; +import {EventDisplayService} from "phoenix-ui-components"; +import {MenuToggleComponent} from "../menu-toggle/menu-toggle.component"; + +@Component({ + selector: 'app-custom-dark-theme', + templateUrl: './dark-theme.component.html', + styleUrls: ['./dark-theme.component.scss'], + imports: [ + MenuToggleComponent + ], + standalone: true +}) +export class DarkThemeComponent implements OnInit { + darkTheme = false; + + constructor(private eventDisplay: EventDisplayService) {} + + ngOnInit(): void { + this.darkTheme = this.eventDisplay.getUIManager().getDarkTheme(); + } + + setDarkTheme() { + this.darkTheme = !this.darkTheme; + this.eventDisplay.getUIManager().setDarkTheme(this.darkTheme); + } +} diff --git a/firebird-ng/src/app/components/event-selector/event-selector.component.html b/firebird-ng/src/app/components/event-selector/event-selector.component.html new file mode 100644 index 0000000..6d64a52 --- /dev/null +++ b/firebird-ng/src/app/components/event-selector/event-selector.component.html @@ -0,0 +1,12 @@ +
+ +
diff --git a/firebird-ng/src/app/components/event-selector/event-selector.component.scss b/firebird-ng/src/app/components/event-selector/event-selector.component.scss new file mode 100644 index 0000000..e1afa73 --- /dev/null +++ b/firebird-ng/src/app/components/event-selector/event-selector.component.scss @@ -0,0 +1,11 @@ +.eventSelector { + select { + width: 9rem; + padding: 5px 10px; + font-size: 12px; + border: 1px solid rgba(88, 88, 88, 0.08); + box-shadow: var(--phoenix-icon-shadow); + background-color: var(--phoenix-background-color-tertiary); + color: var(--phoenix-text-color-secondary); + } +} diff --git a/firebird-ng/src/app/components/event-selector/event-selector.component.test.ts b/firebird-ng/src/app/components/event-selector/event-selector.component.test.ts new file mode 100644 index 0000000..4ebd7c4 --- /dev/null +++ b/firebird-ng/src/app/components/event-selector/event-selector.component.test.ts @@ -0,0 +1,56 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EventSelectorComponent } from './event-selector.component'; +import { EventDisplayService } from '../../../services/event-display.service'; +import { PhoenixUIModule } from '../../phoenix-ui.module'; + +describe('EventSelectorComponent', () => { + let component: EventSelectorComponent; + let fixture: ComponentFixture; + + const mockEventDisplayService = { + listenToLoadedEventsChange: jest.fn(), + loadEvent: jest.fn(), + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PhoenixUIModule], + providers: [ + { + provide: EventDisplayService, + useValue: mockEventDisplayService, + }, + ], + declarations: [EventSelectorComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EventSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize to listen to loaded events change', () => { + component.ngOnInit(); + + expect( + mockEventDisplayService.listenToLoadedEventsChange, + ).toHaveBeenCalled(); + }); + + it('should change event through event display', () => { + const mockSelectEvent = { target: { value: 'TestEvent' } }; + + component.changeEvent(mockSelectEvent); + + expect(mockEventDisplayService.loadEvent).toHaveBeenCalledWith( + mockSelectEvent.target.value, + ); + }); +}); diff --git a/firebird-ng/src/app/components/event-selector/event-selector.component.ts b/firebird-ng/src/app/components/event-selector/event-selector.component.ts new file mode 100644 index 0000000..4594653 --- /dev/null +++ b/firebird-ng/src/app/components/event-selector/event-selector.component.ts @@ -0,0 +1,34 @@ +import { Component, type OnInit } from '@angular/core'; +import {EventDisplayService} from "phoenix-ui-components"; +import {MatTooltip} from "@angular/material/tooltip"; +import {NgForOf, NgIf} from "@angular/common"; + + +@Component({ + selector: 'app-custom-event-selector', + templateUrl: './event-selector.component.html', + styleUrls: ['./event-selector.component.scss'], + imports: [ + MatTooltip, + NgForOf, + NgIf + ], + standalone: true +}) +export class EventSelectorComponent implements OnInit { + // Array containing the keys of the multiple loaded events + events: string[] = []; + + constructor(private eventDisplay: EventDisplayService) {} + + ngOnInit() { + this.eventDisplay.listenToLoadedEventsChange( + (events) => (this.events = events), + ); + } + + changeEvent(selected: any) { + const value = selected.target.value; + this.eventDisplay.loadEvent(value); + } +} diff --git a/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.html b/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.html new file mode 100644 index 0000000..66aeb51 --- /dev/null +++ b/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.html @@ -0,0 +1,8 @@ + + diff --git a/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.scss b/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.test.ts b/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.test.ts new file mode 100644 index 0000000..ce19eda --- /dev/null +++ b/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.test.ts @@ -0,0 +1,49 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MainViewToggleComponent } from './main-view-toggle.component'; +import { EventDisplayService } from '../../../services/event-display.service'; +import { PhoenixUIModule } from '../../phoenix-ui.module'; + +describe('MainViewToggleComponent', () => { + let component: MainViewToggleComponent; + let fixture: ComponentFixture; + + const mockEventDisplay = { + getUIManager: jest.fn().mockReturnThis(), + toggleOrthographicView: jest.fn().mockReturnThis(), + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PhoenixUIModule], + providers: [ + { + provide: EventDisplayService, + useValue: mockEventDisplay, + }, + ], + declarations: [MainViewToggleComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MainViewToggleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should switch main view', () => { + expect(component.orthographicView).toBe(false); + + component.switchMainView(); + + expect(component.orthographicView).toBe(true); + expect( + mockEventDisplay.getUIManager().toggleOrthographicView, + ).toHaveBeenCalled(); + }); +}); diff --git a/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.ts b/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.ts new file mode 100644 index 0000000..12aa669 --- /dev/null +++ b/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; +import {EventDisplayService} from "phoenix-ui-components"; +import {MenuToggleComponent} from "../menu-toggle/menu-toggle.component"; +import {MatTooltip} from "@angular/material/tooltip"; + + +@Component({ + selector: 'app-custom-main-view-toggle', + templateUrl: './main-view-toggle.component.html', + styleUrls: ['./main-view-toggle.component.scss'], + imports: [ + MenuToggleComponent, + MatTooltip + ], + standalone: true +}) +export class MainViewToggleComponent { + orthographicView: boolean = false; + + constructor(private eventDisplay: EventDisplayService) {} + + switchMainView() { + this.orthographicView = !this.orthographicView; + this.eventDisplay + .getUIManager() + .toggleOrthographicView(this.orthographicView); + } +} diff --git a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.html b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.html new file mode 100644 index 0000000..fcdd471 --- /dev/null +++ b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.html @@ -0,0 +1,11 @@ + diff --git a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss new file mode 100644 index 0000000..0528817 --- /dev/null +++ b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss @@ -0,0 +1,38 @@ +:host { + display: flex; + margin: 0 0.6rem; + + .menu-toggle { + display: flex; + background: unset; + border: none; + height: 2.5rem; + width: 2.5rem; + min-height: 2.5rem; + min-width: 2.5rem; + padding: 0.65rem; + cursor: pointer; + align-self: center; + transition: all 0.4s; + + &-icon { + width: 100%; + height: 100%; + + &.active-icon { + --phoenix-options-icon-path: #00bcd4; + } + } + + &:hover { + background-color: var(--phoenix-options-icon-bg); + border-radius: 40%; + transition: all 0.4s; + } + + &.disabled { + cursor: not-allowed; + opacity: 0.4; + } + } +} diff --git a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.test.ts b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.test.ts new file mode 100644 index 0000000..570bdf8 --- /dev/null +++ b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.test.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PhoenixUIModule } from '../../phoenix-ui.module'; + +import { MenuToggleComponent } from './menu-toggle.component'; + +describe('MenuToggleComponent', () => { + let component: MenuToggleComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PhoenixUIModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MenuToggleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.ts b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.ts new file mode 100644 index 0000000..e2b8b83 --- /dev/null +++ b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.ts @@ -0,0 +1,20 @@ +import { Component, Input } from '@angular/core'; +import {NgClass} from "@angular/common"; +import {MatTooltip} from "@angular/material/tooltip"; + +@Component({ + selector: 'app-custom-menu-toggle', + templateUrl: './menu-toggle.component.html', + styleUrls: ['./menu-toggle.component.scss'], + imports: [ + NgClass, + MatTooltip + ], + standalone: true +}) +export class MenuToggleComponent { + @Input() icon: string = ''; + @Input() active: boolean = false; + @Input() tooltip: string = ''; + @Input() disabled: boolean = false; +} diff --git a/firebird-ng/src/app/components/object-clipping/object-clipping.component.html b/firebird-ng/src/app/components/object-clipping/object-clipping.component.html new file mode 100644 index 0000000..6f154ac --- /dev/null +++ b/firebird-ng/src/app/components/object-clipping/object-clipping.component.html @@ -0,0 +1,52 @@ + + + + + + + + diff --git a/firebird-ng/src/app/components/object-clipping/object-clipping.component.scss b/firebird-ng/src/app/components/object-clipping/object-clipping.component.scss new file mode 100644 index 0000000..5a4ad30 --- /dev/null +++ b/firebird-ng/src/app/components/object-clipping/object-clipping.component.scss @@ -0,0 +1,8 @@ +.slider-btn { + overflow: visible; +} + +mat-slider { + margin-left: 0.75rem; + margin-right: 0.75rem; +} diff --git a/firebird-ng/src/app/components/object-clipping/object-clipping.component.test.ts b/firebird-ng/src/app/components/object-clipping/object-clipping.component.test.ts new file mode 100644 index 0000000..bedcf6a --- /dev/null +++ b/firebird-ng/src/app/components/object-clipping/object-clipping.component.test.ts @@ -0,0 +1,84 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ObjectClippingComponent } from './object-clipping.component'; +import { MatCheckboxChange } from '@angular/material/checkbox'; +import { EventDisplayService } from '../../../services/event-display.service'; +import { PhoenixUIModule } from '../../phoenix-ui.module'; + +describe('ObjectClippingComponent', () => { + let component: ObjectClippingComponent; + let fixture: ComponentFixture; + + const mockUIManager = { + rotateStartAngleClipping: jest.fn(), + rotateOpeningAngleClipping: jest.fn(), + setClipping: jest.fn(), + }; + + const mockEventDisplay = { + getUIManager: jest.fn(() => mockUIManager), + getStateManager: () => ({ + clippingEnabled: { + onUpdate: jest.fn(), + }, + startClippingAngle: { + onUpdate: jest.fn(), + }, + openingClippingAngle: { + onUpdate: jest.fn(), + }, + }), + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PhoenixUIModule], + providers: [ + { + provide: EventDisplayService, + useValue: mockEventDisplay, + }, + ], + declarations: [ObjectClippingComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ObjectClippingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should toggle clipping', () => { + expect(component.clippingEnabled).toBeUndefined(); + const matCheckboxChange = new MatCheckboxChange(); + + // Check for true + matCheckboxChange.checked = true; + component.toggleClipping(matCheckboxChange); + expect(component.clippingEnabled).toBe(true); + + // Check for false + matCheckboxChange.checked = false; + component.toggleClipping(matCheckboxChange); + expect(component.clippingEnabled).toBe(false); + }); + + it('should change clipping angle', () => { + const sliderValue = 10; + + component.changeStartClippingAngle(sliderValue); + expect( + mockEventDisplay.getUIManager().rotateStartAngleClipping, + ).toHaveBeenCalledWith(sliderValue); + + component.changeOpeningClippingAngle(sliderValue); + expect( + mockEventDisplay.getUIManager().rotateOpeningAngleClipping, + ).toHaveBeenCalledWith(sliderValue); + }); +}); diff --git a/firebird-ng/src/app/components/object-clipping/object-clipping.component.ts b/firebird-ng/src/app/components/object-clipping/object-clipping.component.ts new file mode 100644 index 0000000..480b504 --- /dev/null +++ b/firebird-ng/src/app/components/object-clipping/object-clipping.component.ts @@ -0,0 +1,54 @@ +import { Component } from '@angular/core'; +import {MatCheckbox, MatCheckboxChange} from '@angular/material/checkbox'; +import {EventDisplayService} from "phoenix-ui-components"; +import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu"; +import {MatSlider, MatSliderThumb} from "@angular/material/slider"; +import {MenuToggleComponent} from "../menu-toggle/menu-toggle.component"; + +@Component({ + selector: 'app-custom-object-clipping', + templateUrl: './object-clipping.component.html', + styleUrls: ['./object-clipping.component.scss'], + imports: [ + MatMenu, + MatCheckbox, + MatMenuItem, + MatSlider, + MatSliderThumb, + MenuToggleComponent, + MatMenuTrigger + ], + standalone: true +}) +export class ObjectClippingComponent { + clippingEnabled!: boolean; + startClippingAngle!: number; + openingClippingAngle!: number; + + constructor(private eventDisplay: EventDisplayService) { + const stateManager = this.eventDisplay.getStateManager(); + stateManager.clippingEnabled.onUpdate( + (clippingValue) => (this.clippingEnabled = clippingValue), + ); + stateManager.startClippingAngle.onUpdate( + (value) => (this.startClippingAngle = value), + ); + stateManager.openingClippingAngle.onUpdate( + (value) => (this.openingClippingAngle = value), + ); + } + + changeStartClippingAngle(startingAngle: number) { + this.eventDisplay.getUIManager().rotateStartAngleClipping(startingAngle); + } + + changeOpeningClippingAngle(openingAngle: number) { + this.eventDisplay.getUIManager().rotateOpeningAngleClipping(openingAngle); + } + + toggleClipping(change: MatCheckboxChange) { + const value = change.checked; + this.eventDisplay.getUIManager().setClipping(value); + this.clippingEnabled = value; + } +} diff --git a/firebird-ng/src/app/components/tool-panel/tool-panel.component.html b/firebird-ng/src/app/components/tool-panel/tool-panel.component.html index 05d390b..cab3b4c 100644 --- a/firebird-ng/src/app/components/tool-panel/tool-panel.component.html +++ b/firebird-ng/src/app/components/tool-panel/tool-panel.component.html @@ -21,8 +21,8 @@ (touchcancel)="clearZoom()"> zoom_out - - + +
diff --git a/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts b/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts index 0f4e2bc..50d49d5 100644 --- a/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts +++ b/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts @@ -3,6 +3,8 @@ import {NgIf} from "@angular/common"; import {MatIcon} from "@angular/material/icon"; import {EventDisplayService, PhoenixUIModule} from 'phoenix-ui-components'; import {PhoenixThreeFacade} from "../../utils/phoenix-three-facade"; +import {ViewOptionsComponent} from "../view-options/view-options.component"; +import {MainViewToggleComponent} from "../main-view-toggle/main-view-toggle.component"; @Component({ selector: 'app-tool-panel', @@ -10,7 +12,9 @@ import {PhoenixThreeFacade} from "../../utils/phoenix-three-facade"; imports: [ NgIf, MatIcon, - PhoenixUIModule + PhoenixUIModule, + ViewOptionsComponent, + MainViewToggleComponent ], templateUrl: './tool-panel.component.html', styleUrl: './tool-panel.component.scss' diff --git a/firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.html b/firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.html new file mode 100644 index 0000000..4a62424 --- /dev/null +++ b/firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.html @@ -0,0 +1,157 @@ +
+

Customize Cartesian Grid

+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+ +
+ + + + + + + +
+
+ +
+ +
+
diff --git a/firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.scss b/firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.scss new file mode 100644 index 0000000..815210d --- /dev/null +++ b/firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.scss @@ -0,0 +1,38 @@ +.container { + display: flex; + flex-direction: row; +} + +.item-config-single { + display: flex; + flex-direction: column; + + .item-config-group { + margin: 0.1rem 2rem; + + .item-config-label { + display: inline; + margin: 1rem; + } + + .form-control { + width: 60%; + display: inline; + } + } + + button { + margin: 0.5rem 4rem; + } + + .explain-button { + padding-top: 1rem; + } + + .explain-text { + width: 20rem; + opacity: 0.7; + margin-top: 0.5rem; + font-size: 0.9rem; + } +} diff --git a/firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.test.ts b/firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.test.ts new file mode 100644 index 0000000..080f3c4 --- /dev/null +++ b/firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.test.ts @@ -0,0 +1,269 @@ +import { ComponentFixture, TestBed, tick } from '@angular/core/testing'; + +import { CartesianGridConfigComponent } from './cartesian-grid-config.component'; +import { EventDisplayService, PhoenixUIModule } from 'phoenix-ui-components'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatCheckboxChange } from '@angular/material/checkbox'; +import { Vector3 } from 'three'; +import { of } from 'rxjs/internal/observable/of'; + +describe('CartesianGridConfigComponent', () => { + let component: CartesianGridConfigComponent; + let fixture: ComponentFixture; + + const mockDialogRef = { + close: jest.fn().mockReturnThis(), + }; + + const gridOrigin = new Vector3(100, 200, 300); + + const mockEventDisplay = { + getUIManager: jest.fn().mockReturnThis(), + translateCartesianGrid: jest.fn().mockReturnThis(), + translateCartesianLabels: jest.fn().mockReturnThis(), + getCartesianGridConfig: jest.fn().mockReturnValue({ + showXY: true, + showYZ: true, + showZX: true, + xDistance: 300, + yDistance: 300, + zDistance: 300, + sparsity: 2, + }), + setShowCartesianGrid: jest.fn().mockReturnThis(), + shiftCartesianGridByPointer: jest.fn().mockReturnThis(), + getThreeManager: jest.fn().mockReturnThis(), + originChanged: of(gridOrigin), + stopShifting: of(true), + origin: new Vector3(0, 0, 0), + originChangedEmit: jest.fn().mockReturnThis(), + }; + + const mockData = { + gridVisible: true, + scale: 3000, + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [PhoenixUIModule], + declarations: [CartesianGridConfigComponent], + providers: [ + { + provide: MatDialogRef, + useValue: mockDialogRef, + }, + { + provide: EventDisplayService, + useValue: mockEventDisplay, + }, + { + provide: MAT_DIALOG_DATA, + useValue: mockData, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(CartesianGridConfigComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set initial configuration', (done) => { + const VALUE1 = component.data.gridVisible; + const VALUE2 = component.data.scale; + + component.ngOnInit(); + + expect(component.showCartesianGrid).toBe(VALUE1); + expect(component.scale).toBe(VALUE2); + + expect( + mockEventDisplay.getUIManager().getCartesianGridConfig, + ).toHaveBeenCalled(); + + const VALUE3 = component.gridConfig; + + expect( + mockEventDisplay.getUIManager().getCartesianGridConfig, + ).toHaveReturnedWith(VALUE3); + + expect(mockEventDisplay.getThreeManager).toHaveBeenCalled(); + + const VALUE4 = component.cartesianPos; + + expect(mockEventDisplay.getThreeManager().origin).toBe(VALUE4); + done(); + }); + + it('should close', () => { + component.onClose(); + + expect(mockDialogRef.close).toHaveBeenCalled(); + }); + + it('should save the updated grid origin', () => { + const VALUE1 = 10; + const VALUE2 = 20; + const VALUE3 = 30; + + const spy = jest.spyOn(component, 'shiftCartesianGridByValues'); + + component.onSave(VALUE1, VALUE2, VALUE3); + + expect(spy).toHaveBeenCalledWith( + new Vector3(VALUE1 * 10, VALUE2 * 10, VALUE3 * 10), + ); + }); + + it('should shift cartesian grid by a mouse click', () => { + component.shiftCartesianGridByPointer(); + + mockEventDisplay.getUIManager().shiftCartesianGridByPointer(true); + + mockEventDisplay.getThreeManager().originChanged.subscribe((intersect) => { + expect(component.translateGrid).toHaveBeenCalledWith(intersect); + }); + + const originChangedUnSpy = jest.spyOn( + component.originChangedSub, + 'unsubscribe', + ); + const stopShiftingUnSpy = jest.spyOn( + component.stopShiftingSub, + 'unsubscribe', + ); + + mockEventDisplay.getThreeManager().stopShifting.subscribe((stop) => { + if (stop) { + expect(originChangedUnSpy).toHaveBeenCalled(); + expect(stopShiftingUnSpy).toHaveBeenCalled(); + } + }); + }); + + it('should shift cartesian grid by values', () => { + const VALUE = new Vector3(100, 200, 300); + + const spy = jest.spyOn(component, 'translateGrid'); + + component.shiftCartesianGridByValues(VALUE); + + expect(spy).toHaveBeenCalledWith(VALUE); + expect( + mockEventDisplay.getThreeManager().originChangedEmit, + ).toHaveBeenCalledWith(VALUE); + }); + + it('should translate grid', () => { + const VALUE1 = new Vector3(100, 200, 300); + + const finalPos = VALUE1; + const initialPos = component.cartesianPos; + const difference = new Vector3( + finalPos.x - initialPos.x, + finalPos.y - initialPos.y, + finalPos.z - initialPos.z, + ); + + component['translateGrid'](VALUE1); + + expect( + mockEventDisplay.getUIManager().translateCartesianGrid, + ).toHaveBeenCalledWith(difference.clone()); + expect( + mockEventDisplay.getUIManager().translateCartesianLabels, + ).toHaveBeenCalledWith(difference.clone()); + expect(component.cartesianPos).toBe(finalPos); + }); + + it('should add XY Planes', () => { + const event = { target: { value: '600' } } as any; + const VALUE = Number(event.target.value); + + const spy = jest.spyOn(component, 'callSetShowCartesianGrid'); + + component.addXYPlanes(event); + + expect(component.gridConfig.zDistance).toBe(VALUE); + expect(spy).toHaveBeenCalled(); + }); + + it('should add YZ Planes', () => { + const event = { target: { value: '600' } } as any; + const VALUE = Number(event.target.value); + + const spy = jest.spyOn(component, 'callSetShowCartesianGrid'); + + component.addYZPlanes(event); + + expect(component.gridConfig.xDistance).toBe(VALUE); + expect(spy).toHaveBeenCalled(); + }); + + it('should add ZX Planes', () => { + const event = { target: { value: '600' } } as any; + const VALUE = Number(event.target.value); + + const spy = jest.spyOn(component, 'callSetShowCartesianGrid'); + + component.addZXPlanes(event); + + expect(component.gridConfig.yDistance).toBe(VALUE); + expect(spy).toHaveBeenCalled(); + }); + + it('should change sparsity', () => { + const event = { target: { value: '2' } } as any; + const VALUE = Number(event.target.value); + + const spy = jest.spyOn(component, 'callSetShowCartesianGrid'); + component.changeSparsity(event); + + expect(component.gridConfig.sparsity).toBe(VALUE); + expect(spy).toHaveBeenCalled(); + }); + + it('should show XY Planes', () => { + const VALUE = false; + const event = new MatCheckboxChange(); + event.checked = VALUE; + + component.showXYPlanes(event); + expect(component.gridConfig.showXY).toBe(VALUE); + }); + + it('should show YZ Planes', () => { + const VALUE = false; + const event = new MatCheckboxChange(); + event.checked = VALUE; + + component.showYZPlanes(event); + expect(component.gridConfig.showYZ).toBe(VALUE); + }); + + it('should show ZX Planes', () => { + const VALUE = false; + const event = new MatCheckboxChange(); + event.checked = VALUE; + + component.showZXPlanes(event); + expect(component.gridConfig.showZX).toBe(VALUE); + }); + + it('should call setShowCartesianGrid', () => { + component.callSetShowCartesianGrid(); + + expect( + mockEventDisplay.getUIManager().setShowCartesianGrid, + ).toHaveBeenCalledWith( + component.showCartesianGrid, + component.scale, + component.gridConfig, + ); + }); +}); diff --git a/firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.ts b/firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.ts new file mode 100644 index 0000000..8992e8a --- /dev/null +++ b/firebird-ng/src/app/components/view-options/cartesian-grid-config/cartesian-grid-config.component.ts @@ -0,0 +1,179 @@ +import { Component, Inject, type OnInit } from '@angular/core'; +import { Vector3 } from 'three'; +import {MatCheckbox, MatCheckboxChange} from '@angular/material/checkbox'; +import { + MAT_DIALOG_DATA, + MatDialogActions, + MatDialogContent, + MatDialogRef, + MatDialogTitle +} from '@angular/material/dialog'; +import {EventDisplayService} from "phoenix-ui-components"; +import { Subscription } from 'rxjs'; +import {DecimalPipe} from "@angular/common"; +import {MatButton} from "@angular/material/button"; +import {MatMenuItem} from "@angular/material/menu"; +import {FormsModule} from "@angular/forms"; +import {MatSlider, MatSliderThumb} from "@angular/material/slider"; + +@Component({ + selector: 'app-cartesian-grid-config', + templateUrl: './cartesian-grid-config.component.html', + styleUrls: ['./cartesian-grid-config.component.scss'], + imports: [ + MatDialogTitle, + MatDialogContent, + DecimalPipe, + MatButton, + MatMenuItem, + MatCheckbox, + FormsModule, + MatSlider, + MatSliderThumb, + MatDialogActions + ], + standalone: true +}) +export class CartesianGridConfigComponent implements OnInit { + cartesianPos = new Vector3(); + originChangedSub: Subscription | null = null; + stopShiftingSub: Subscription | null = null; + showCartesianGrid!: boolean; + gridConfig!: { + showXY: boolean; + showYZ: boolean; + showZX: boolean; + xDistance: number; + yDistance: number; + zDistance: number; + sparsity: number; + }; + scale!: number; + shiftGrid!: boolean; + + constructor( + @Inject(MAT_DIALOG_DATA) + public data: { gridVisible: boolean; scale: number }, + private dialogRef: MatDialogRef, + private eventDisplay: EventDisplayService, + ) {} + + ngOnInit(): void { + this.shiftGrid = this.eventDisplay.getThreeManager().shiftGrid; + this.showCartesianGrid = this.data.gridVisible; + this.scale = this.data.scale; + this.gridConfig = this.eventDisplay.getUIManager().getCartesianGridConfig(); + this.cartesianPos = this.eventDisplay.getThreeManager().origin; + } + + onClose() { + this.dialogRef.close(); + } + + onSave(x: string, y: string, z: string) { + const xNum = Number(x); + const yNum = Number(y); + const zNum = Number(z); + this.shiftCartesianGridByValues(new Vector3(xNum * 10, yNum * 10, zNum * 10)); + } + + shiftCartesianGridByPointer() { + this.shiftGrid = true; + this.eventDisplay.getUIManager().shiftCartesianGridByPointer(); + this.originChangedSub = this.eventDisplay + .getThreeManager() + .originChanged.subscribe((intersect) => { + this.translateGrid(intersect); + }); + this.stopShiftingSub = this.eventDisplay + .getThreeManager() + .stopShifting.subscribe((stop) => { + if (stop) { + this.originChangedSub?.unsubscribe(); + this.stopShiftingSub?.unsubscribe(); + } + }); + this.onClose(); + } + + shiftCartesianGridByValues(position: Vector3) { + this.translateGrid(position); + this.eventDisplay.getThreeManager().originChangedEmit(position); + } + + translateGrid(position: Vector3) { + const finalPos = position; + const initialPos = this.cartesianPos; + const difference = new Vector3( + finalPos.x - initialPos.x, + finalPos.y - initialPos.y, + finalPos.z - initialPos.z, + ); + this.eventDisplay.getUIManager().translateCartesianGrid(difference.clone()); + this.eventDisplay + .getUIManager() + .translateCartesianLabels(difference.clone()); + this.cartesianPos = finalPos; + } + + addXYPlanes(zDistance: Event) { + this.gridConfig.zDistance = Number( + (zDistance.target as HTMLInputElement).value, + ); + this.callSetShowCartesianGrid(); + } + + addYZPlanes(xDistance: Event) { + this.gridConfig.xDistance = Number( + (xDistance.target as HTMLInputElement).value, + ); + this.callSetShowCartesianGrid(); + } + + addZXPlanes(yDistance: Event) { + this.gridConfig.yDistance = Number( + (yDistance.target as HTMLInputElement).value, + ); + this.callSetShowCartesianGrid(); + } + + changeSparsity(sparsity: Event) { + this.gridConfig.sparsity = Number( + (sparsity.target as HTMLInputElement).value, + ); + this.callSetShowCartesianGrid(); + } + + showXYPlanes(change: MatCheckboxChange) { + this.gridConfig.showXY = change.checked; + this.callSetShowCartesianGrid(); + } + + showYZPlanes(change: MatCheckboxChange) { + this.gridConfig.showYZ = change.checked; + this.callSetShowCartesianGrid(); + } + + showZXPlanes(change: MatCheckboxChange) { + this.gridConfig.showZX = change.checked; + this.callSetShowCartesianGrid(); + } + + callSetShowCartesianGrid() { + this.eventDisplay + .getUIManager() + .setShowCartesianGrid( + this.showCartesianGrid, + this.scale, + this.gridConfig, + ); + } + + // helper function to calculate number of planes + calcPlanes(dis: number) { + return Math.max( + 0, + 1 + 2 * Math.floor((dis * 10) / (this.scale * this.gridConfig.sparsity)), + ); + } +} diff --git a/firebird-ng/src/app/components/view-options/view-options.component.html b/firebird-ng/src/app/components/view-options/view-options.component.html new file mode 100644 index 0000000..4619b3b --- /dev/null +++ b/firebird-ng/src/app/components/view-options/view-options.component.html @@ -0,0 +1,103 @@ + + + + + + + + + + + + + diff --git a/firebird-ng/src/app/components/view-options/view-options.component.scss b/firebird-ng/src/app/components/view-options/view-options.component.scss new file mode 100644 index 0000000..980a33d --- /dev/null +++ b/firebird-ng/src/app/components/view-options/view-options.component.scss @@ -0,0 +1,31 @@ +.view-icon { + width: 1.2rem; + height: 1.2rem; + margin-right: 0.5rem; +} + +.icon-wrapper { + display: inline-block; + width: 1.5rem; + height: 1.5rem; + padding: 0.23rem; + transition: all 0.4s; + transform: translateY(27%); + + &.icon-button:hover { + background: var(--phoenix-options-icon-bg); + border-radius: 40%; + cursor: pointer; + } + + svg { + width: 100%; + height: 100%; + vertical-align: top; + } +} + +.item-settings { + margin-right: 0.2rem; + margin-left: 0.5rem; +} diff --git a/firebird-ng/src/app/components/view-options/view-options.component.test.ts b/firebird-ng/src/app/components/view-options/view-options.component.test.ts new file mode 100644 index 0000000..62c40d6 --- /dev/null +++ b/firebird-ng/src/app/components/view-options/view-options.component.test.ts @@ -0,0 +1,193 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PresetView } from 'phoenix-event-display'; +import { ViewOptionsComponent } from './view-options.component'; +import { + EventDisplayService, + PhoenixUIModule, + CartesianGridConfigComponent, +} from 'phoenix-ui-components'; +import { MatCheckboxChange } from '@angular/material/checkbox'; +import { MatDialog } from '@angular/material/dialog'; +import { Vector3 } from 'three'; +import { of } from 'rxjs/internal/observable/of'; +import { Subscription } from 'rxjs'; + +describe('ViewOptionsComponent', () => { + let component: ViewOptionsComponent; + let fixture: ComponentFixture; + + const origin = new Vector3(100, 200, 300); + + const mockEventDisplay = { + getUIManager: jest.fn().mockReturnThis(), + getThreeManager: jest.fn().mockReturnThis(), + getPresetViews: jest.fn().mockReturnValue([]), + displayView: jest.fn().mockReturnThis(), + setShowAxis: jest.fn().mockReturnThis(), + setShowEtaPhiGrid: jest.fn().mockReturnThis(), + setShowCartesianGrid: jest.fn().mockReturnThis(), + showLabels: jest.fn().mockReturnThis(), + show3DMousePoints: jest.fn().mockReturnThis(), + show3DDistance: jest.fn().mockReturnThis(), + originChanged: of(origin), + }; + + const mockMatDialog = { + open: jest.fn().mockReturnThis(), + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PhoenixUIModule], + declarations: [ViewOptionsComponent, CartesianGridConfigComponent], + providers: [ + { + provide: EventDisplayService, + useValue: mockEventDisplay, + }, + { + provide: MatDialog, + useValue: mockMatDialog, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ViewOptionsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initially get preset views and set up the subscription', () => { + component.ngOnInit(); + + expect(mockEventDisplay.getUIManager().getPresetViews).toHaveBeenCalled(); + + mockEventDisplay.getThreeManager().originChanged.subscribe((intersect) => { + expect(component.origin).toBe(intersect); + }); + }); + + it('should open cartesian grid config dialog box', () => { + const mockParams = { + data: { + gridVisible: component.showCartesianGrid, + scale: component.scale, + }, + position: { + bottom: '5rem', + left: '3rem', + }, + }; + + component.openCartesianGridConfigDialog(); + + expect(mockMatDialog.open).toHaveBeenCalledWith( + CartesianGridConfigComponent, + mockParams, + ); + }); + + it('should display the chosen preset view', () => { + const mockEvent = { + stopPropagation: jest.fn(), + }; + const mockPresetView = new PresetView( + 'Test View', + [0, 0, -12000], + [0, 0, 0], + 'left-cube', + ); + component.displayView(mockEvent, mockPresetView); + + expect(mockEventDisplay.getUIManager().displayView).toHaveBeenCalledWith( + mockPresetView, + ); + }); + + it('should set axis', () => { + const VALUE = false; + const event = new MatCheckboxChange(); + event.checked = VALUE; + + component.setAxis(event); + + expect(mockEventDisplay.getUIManager().setShowAxis).toHaveBeenCalledWith( + VALUE, + ); + }); + + it('should set eta phi grid', () => { + const VALUE = false; + const event = new MatCheckboxChange(); + event.checked = VALUE; + + component.setEtaPhiGrid(event); + + expect( + mockEventDisplay.getUIManager().setShowEtaPhiGrid, + ).toHaveBeenCalledWith(VALUE); + }); + + it('should set cartesian grid', () => { + const VALUE = false; + const event = new MatCheckboxChange(); + event.checked = VALUE; + + component.setCartesianGrid(event); + + expect(component.showCartesianGrid).toBe(false); + expect( + mockEventDisplay.getUIManager().setShowCartesianGrid, + ).toHaveBeenCalledWith(VALUE, component.scale); + }); + + it('should show labels', () => { + const VALUE = false; + const event = new MatCheckboxChange(); + event.checked = VALUE; + + component.showLabels(event); + + expect(mockEventDisplay.getUIManager().showLabels).toHaveBeenCalledWith( + VALUE, + ); + }); + + it('should show 3D mouse points', () => { + const VALUE = true; + const event = new MatCheckboxChange(); + event.checked = VALUE; + + component.show3DMousePoints(event); + + expect(component.show3DPoints).toBe(VALUE); + expect( + mockEventDisplay.getUIManager().show3DMousePoints, + ).toHaveBeenCalledWith(component.show3DPoints); + }); + + it('should toggle the show-distance function', () => { + const VALUE = true; + const event = new MatCheckboxChange(); + event.checked = VALUE; + + component.toggleShowDistance(event); + + expect(mockEventDisplay.getUIManager().show3DDistance).toHaveBeenCalledWith( + VALUE, + ); + }); + + it('should unsubscribe the existing subscriptions', () => { + component.sub = new Subscription(); + const spy = jest.spyOn(component.sub, 'unsubscribe'); + + component.ngOnDestroy(); + + expect(spy).toHaveBeenCalledTimes(1); + }); +}); diff --git a/firebird-ng/src/app/components/view-options/view-options.component.ts b/firebird-ng/src/app/components/view-options/view-options.component.ts new file mode 100644 index 0000000..eb5eaee --- /dev/null +++ b/firebird-ng/src/app/components/view-options/view-options.component.ts @@ -0,0 +1,108 @@ +import { + Component, + type OnInit, + type OnDestroy, + ViewChild, +} from '@angular/core'; +import { PresetView } from 'phoenix-event-display'; +import {MatCheckbox, MatCheckboxChange} from '@angular/material/checkbox'; +import {EventDisplayService} from "phoenix-ui-components"; +import { MatDialog } from '@angular/material/dialog'; +import { CartesianGridConfigComponent } from './cartesian-grid-config/cartesian-grid-config.component'; +import { Subscription } from 'rxjs'; +import { Vector3 } from 'three'; +import {MatMenu, MatMenuItem, MatMenuTrigger} from '@angular/material/menu'; +import {NgForOf, NgIf} from "@angular/common"; +import {MenuToggleComponent} from "../menu-toggle/menu-toggle.component"; + +@Component({ + selector: 'app-custom-view-options', + templateUrl: './view-options.component.html', + styleUrls: ['./view-options.component.scss'], + imports: [ + MatMenu, + MatCheckbox, + MatMenuItem, + NgForOf, + MenuToggleComponent, + MatMenuTrigger, + NgIf + ], + standalone: true +}) +export class ViewOptionsComponent implements OnInit, OnDestroy { + @ViewChild(MatMenuTrigger) trigger!: MatMenuTrigger; + showCartesianGrid: boolean = false; + scale: number = 3000; + views!: PresetView[]; + show3DPoints!: boolean; + origin: Vector3 = new Vector3(0, 0, 0); + sub!: Subscription; + + constructor( + private eventDisplay: EventDisplayService, + private dialog: MatDialog, + ) {} + + ngOnInit(): void { + this.views = this.eventDisplay.getUIManager().getPresetViews(); + this.sub = this.eventDisplay + .getThreeManager() + .originChanged.subscribe((intersect) => { + this.origin = intersect; + }); + } + + openCartesianGridConfigDialog() { + this.dialog.open(CartesianGridConfigComponent, { + data: { + gridVisible: this.showCartesianGrid, + scale: this.scale, + }, + position: { + bottom: '5rem', + left: '3rem', + }, + }); + } + + displayView($event: any, view: PresetView) { + $event.stopPropagation(); + this.eventDisplay.getUIManager().displayView(view); + } + + setAxis(change: MatCheckboxChange) { + const value = change.checked; + this.eventDisplay.getUIManager().setShowAxis(value); + } + + setEtaPhiGrid(change: MatCheckboxChange) { + const value = change.checked; + this.eventDisplay.getUIManager().setShowEtaPhiGrid(value); + } + + setCartesianGrid(change: MatCheckboxChange) { + this.showCartesianGrid = change.checked; + this.eventDisplay + .getUIManager() + .setShowCartesianGrid(this.showCartesianGrid, this.scale); + } + + showLabels(change: MatCheckboxChange) { + this.eventDisplay.getUIManager().showLabels(change.checked); + } + + show3DMousePoints(change: MatCheckboxChange) { + this.show3DPoints = change.checked; + this.eventDisplay.getUIManager().show3DMousePoints(this.show3DPoints); + } + + toggleShowDistance(change: MatCheckboxChange) { + this.trigger.closeMenu(); + this.eventDisplay.getUIManager().show3DDistance(change.checked); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } +} diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.html b/firebird-ng/src/app/pages/main-display/main-display.component.html index aaa711c..1f337bf 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.html +++ b/firebird-ng/src/app/pages/main-display/main-display.component.html @@ -41,27 +41,27 @@
- + - + - + - + - - + + - - + + - - + + - - + + diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.ts b/firebird-ng/src/app/pages/main-display/main-display.component.ts index a444f2a..89c8f1b 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.ts +++ b/firebird-ng/src/app/pages/main-display/main-display.component.ts @@ -4,8 +4,7 @@ import {HttpClient, HttpClientModule} from '@angular/common/http'; import { EventDataFormat, EventDataImportOption, - EventDisplayService, - PhoenixUIModule + EventDisplayService } from 'phoenix-ui-components'; import {ClippingSetting, Configuration, PhoenixLoader, PhoenixMenuNode, PresetView} from 'phoenix-event-display'; import * as THREE from 'three'; @@ -50,6 +49,10 @@ import {AppComponent} from "../../app.component"; import {ToolPanelComponent} from "../../components/tool-panel/tool-panel.component"; import {NavConfigComponent} from "../../components/nav-config/nav-config.component"; import {UrlService} from "../../services/url.service"; +import {EventSelectorComponent} from "../../components/event-selector/event-selector.component"; +import {AutoRotateComponent} from "../../components/auto-rotate/auto-rotate.component"; +import {DarkThemeComponent} from "../../components/dark-theme/dark-theme.component"; +import {ObjectClippingComponent} from "../../components/object-clipping/object-clipping.component"; // import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; @@ -58,7 +61,7 @@ import {UrlService} from "../../services/url.service"; @Component({ selector: 'app-test-experiment', templateUrl: './main-display.component.html', - imports: [PhoenixUIModule, IoOptionsComponent, MatSlider, MatIcon, MatButton, MatSliderThumb, DecimalPipe, MatTooltip, MatFormField, MatSelect, MatOption, NgForOf, AngularSplitModule, SceneTreeComponent, NgClass, MatIconButton, DisplayShellComponent, ToolPanelComponent, NavConfigComponent, NgIf], + imports: [IoOptionsComponent, MatSlider, MatIcon, MatButton, MatSliderThumb, DecimalPipe, MatTooltip, MatFormField, MatSelect, MatOption, NgForOf, AngularSplitModule, SceneTreeComponent, NgClass, MatIconButton, DisplayShellComponent, ToolPanelComponent, NavConfigComponent, NgIf, EventSelectorComponent, AutoRotateComponent, DarkThemeComponent, ObjectClippingComponent], standalone: true, styleUrls: ['./main-display.component.scss'] }) From 00dfbfc5fd29c7ad06f9aeb4d9a554c746aac6b1 Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Wed, 30 Oct 2024 00:51:58 +0200 Subject: [PATCH 10/14] Theme almost works --- .../display-shell.component.scss | 8 +- .../geometry-tree-window.component.scss | 4 +- .../menu-toggle/menu-toggle.component.scss | 13 +- .../nav-config/nav-config.component.scss | 8 +- .../geometry-tree/scene-tree.component.html | 4 +- .../geometry-tree/scene-tree.component.scss | 16 +- .../main-display/main-display.component.html | 22 +-- .../main-display/main-display.component.scss | 17 +- firebird-ng/src/styles.scss | 167 +++++++++++++++++- 9 files changed, 221 insertions(+), 38 deletions(-) diff --git a/firebird-ng/src/app/components/display-shell/display-shell.component.scss b/firebird-ng/src/app/components/display-shell/display-shell.component.scss index 9afb516..c00a782 100644 --- a/firebird-ng/src/app/components/display-shell/display-shell.component.scss +++ b/firebird-ng/src/app/components/display-shell/display-shell.component.scss @@ -14,7 +14,7 @@ flex-direction: row; align-items: center; min-height: 50px; - background-color: #2e2e2e; + //background-color: #2e2e2e; border-bottom: 1px solid #1c1c1c; box-sizing: border-box; } @@ -32,16 +32,16 @@ } .left-pane { - background-color: #2e2e2e; + //background-color: #2e2e2e; } .right-pane { - background-color: #2e2e2e; + //background-color: #2e2e2e; } .central-pane { flex: 1; - background-color: #424242; + //background-color: #424242; overflow: auto; } diff --git a/firebird-ng/src/app/components/geometry-tree-window/geometry-tree-window.component.scss b/firebird-ng/src/app/components/geometry-tree-window/geometry-tree-window.component.scss index 0d947ad..ca48dc8 100644 --- a/firebird-ng/src/app/components/geometry-tree-window/geometry-tree-window.component.scss +++ b/firebird-ng/src/app/components/geometry-tree-window/geometry-tree-window.component.scss @@ -1,5 +1,5 @@ .tree-button { - background-color: #2e2e2e; + //background-color: #2e2e2e; color: white; border-radius: 50%; width: 48px; @@ -23,7 +23,7 @@ .window { width: 400px; - background-color: #2e2e2e; + //background-color: #2e2e2e; } .window-header { diff --git a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss index 0528817..2727a9b 100644 --- a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss +++ b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss @@ -18,14 +18,15 @@ &-icon { width: 100%; height: 100%; + fill: var(--icon-color); &.active-icon { - --phoenix-options-icon-path: #00bcd4; + fill: var(--active-icon-color); } } &:hover { - background-color: var(--phoenix-options-icon-bg); + background-color: var(--icon-bg); border-radius: 40%; transition: all 0.4s; } @@ -36,3 +37,11 @@ } } } +.menu-toggle-icon{ + color: var(--text-color); + transition: background-color 0.3s, color 0.3s; +} + +.button_theme:hover { + color: var(--accent-color); +} diff --git a/firebird-ng/src/app/components/nav-config/nav-config.component.scss b/firebird-ng/src/app/components/nav-config/nav-config.component.scss index a42d94d..69f66a8 100644 --- a/firebird-ng/src/app/components/nav-config/nav-config.component.scss +++ b/firebird-ng/src/app/components/nav-config/nav-config.component.scss @@ -3,12 +3,14 @@ flex-direction: row; box-sizing: border-box; height: 50px; - color: white; flex-wrap: nowrap; align-items: center; - justify-content: space-between; } +.nav-link{ + color: var(--text-color) !important; + //transition: background-color 0.3s, color 0.3s; +} .config-toggle-btn { color: white; position: absolute; @@ -24,3 +26,5 @@ margin: 0; cursor: pointer; } + + diff --git a/firebird-ng/src/app/pages/geometry-tree/scene-tree.component.html b/firebird-ng/src/app/pages/geometry-tree/scene-tree.component.html index 52e4bc5..f2011b5 100644 --- a/firebird-ng/src/app/pages/geometry-tree/scene-tree.component.html +++ b/firebird-ng/src/app/pages/geometry-tree/scene-tree.component.html @@ -1,8 +1,8 @@
- -
diff --git a/firebird-ng/src/app/pages/geometry-tree/scene-tree.component.scss b/firebird-ng/src/app/pages/geometry-tree/scene-tree.component.scss index ee217a8..6313ab8 100644 --- a/firebird-ng/src/app/pages/geometry-tree/scene-tree.component.scss +++ b/firebird-ng/src/app/pages/geometry-tree/scene-tree.component.scss @@ -7,11 +7,21 @@ width: 48px; height: 48px; min-width: 48px; - color: white; + //color: white; padding-top: 8px; } -mat-tree{ - background-color: #2e2e2e; +.button_theme { + background-color: var(--background-color); + color: var(--text-color); + transition: background-color 0.3s, color 0.3s; } +.button_theme:hover { + background-color: var(--secondary-background-color); + color: var(--accent-color); +} + +.mat-tree { + background-color: transparent !important; +} diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.html b/firebird-ng/src/app/pages/main-display/main-display.component.html index 1f337bf..169c747 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.html +++ b/firebird-ng/src/app/pages/main-display/main-display.component.html @@ -38,7 +38,7 @@ -
+
@@ -51,18 +51,6 @@ - - - - - - - - - - - - @@ -96,11 +84,11 @@
-
{{currentTime | number:'1.1-1'}}
+
{{currentTime | number:'1.1-1'}}
-
{{maxTime | number:'1.0-0'}} [ns]   
+
{{maxTime | number:'1.0-0'}} [ns]   
-
{{message}}
+
{{message}}
   -
{{currentGeometry}}
+
{{currentGeometry}}
diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.scss b/firebird-ng/src/app/pages/main-display/main-display.component.scss index b4e2840..72ea135 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.scss +++ b/firebird-ng/src/app/pages/main-display/main-display.component.scss @@ -4,12 +4,13 @@ } + .phoenix-menu{ flex: 1 1 auto; display: flex; align-items: center; justify-content: center; - background-color: #2e2e2e; + //background-color: #2e2e2e; flex-wrap: wrap; } @@ -22,10 +23,16 @@ .toggle-btn { position: absolute; - top: 64px; - color: white; + top: 50px; + //color: white; padding-top: 8px; z-index: 10; + color: var(--text-color); + transition: background-color 0.3s, color 0.3s; +} +.toggle-btn:hover { + background-color: var(--secondary-background-color); + color: var(--accent-color); } .toggle-btn1 { @@ -50,14 +57,14 @@ .time-controls { flex: 1 1 auto; display: flex; - background-color: #424242; + //background-color: #424242; padding: 2px; gap: 3px; align-items: center; justify-content: center; height: 50px; bottom: 0; - color: white; + //color: white; //border-top: 1px solid #ddd; } diff --git a/firebird-ng/src/styles.scss b/firebird-ng/src/styles.scss index 12842fe..dc08080 100644 --- a/firebird-ng/src/styles.scss +++ b/firebird-ng/src/styles.scss @@ -1,2 +1,167 @@ /* You can add global styles to this file, and also import other style files */ -@import 'phoenix-ui-components/theming'; +//@import 'phoenix-ui-components/theming'; + +@use '@angular/material' as mat; +@include mat.core(); + +/* Определяем светлую и тёмную темы */ +$primary: mat.define-palette(mat.$teal-palette); +$accent: mat.define-palette(mat.$cyan-palette); +$warn: mat.define-palette(mat.$yellow-palette); + +$colors: ( + color: ( + primary: $primary, + accent: $accent, + warn: $warn, + ), +); + +$dark-theme: mat.define-dark-theme($colors); +$light-theme: mat.define-light-theme($colors); + +@include mat.core-theme($light-theme); +@include mat.button-theme($light-theme); +@include mat.all-component-themes($light-theme); + +:root { + --primary-color: #33899f; + --secondary-color: #f1e833; + --background-color: #ffffff; + --secondary-background-color: #f5f5f5; + --tertiary-background-color: #e6e6e6; + --text-color: #333333; + --secondary-text-color: #777777; + --hover-text-color: #c5c5c5; + --box-shadow: 0px 8px 20px rgba(0, 0, 0, 0.1); + --scroll-color: #acacac; + --icon-bg: rgba(0, 0, 0, 0.08); + --icon-color: #000000; + --icon-shadow: 0px 3px 6px rgba(0, 0, 0, 0.11); + --accent-color: #00bcd4; + --border-color: #c5c5c5; + --active-icon-color: #1976d2; + + --dat-primary-background: #f9f9f9; + --dat-secondary-background: #efefef; + --dat-tertiary-background: #e9e9e9; + --dat-quaternary-background: #ffffff; + --dat-border-color: #f0f0f0; +} + +/* Тёмная тема */ +[data-theme='dark'] { + --background-color: #2e2e2e; + --secondary-background-color: #292929; + --tertiary-background-color: #3f3f3f; + --text-color: white; + --secondary-text-color: #dbdbdb; + --hover-text-color: #c5c5c5; + --box-shadow: 0px 0px 8px rgb(26, 26, 26); + --scroll-color: #707070; + --icon-bg: rgba(255, 255, 255, 0.18); + --icon-color: #c3c3c3; + --icon-shadow: 0px 4px 8px rgba(0, 0, 0, 0.22); + --accent-color: #00bcd4; + --border-color: #5c5c5c; + + --dat-primary-background: #1a1a1a; + --dat-secondary-background: #292929; + --dat-tertiary-background: #363636; + --dat-quaternary-background: #4d4d4d; + --dat-border-color: #343434; + transition: 1s; + + @include mat.core-color($dark-theme); + @include mat.button-color($dark-theme); + @include mat.all-component-colors($dark-theme); + + .mat-dialog-container { + background: var(--secondary-background-color); + } +} +.theme-text { + color: var(--text-color) !important; /* Добавляем !important, чтобы перекрыть наследуемые стили */ +} + +html, +body { + background-color: var(--background-color); +} + +/* Стили скроллбара */ +::-webkit-scrollbar { + width: 0.3em; + height: 0.3em; +} + +::-webkit-scrollbar-track { + opacity: 0; +} + +::-webkit-scrollbar-thumb { + border-radius: 10px; + background: var(--scroll-color); + box-shadow: 0 0 6px rgba(0, 0, 0, 0.048); +} + +::-webkit-scrollbar-corner { + display: none; + opacity: 0; +} + +.btn.btn-primary { + border: none; + background-color: var(--primary-color); +} + +[data-theme='dark'] .btn.btn-primary:hover, +[data-theme='dark'] .btn.btn-primary:not(:disabled):not(.disabled):active { + background-color: #0f5f74; +} + +[data-theme='light'] .btn.btn-primary:hover, +[data-theme='light'] .btn.btn-primary:not(:disabled):not(.disabled):active { + background-color: #145d7b; +} + +.theme-button { + background-color: var(--button-background) !important; + color: var(--button-text-color) !important; + transition: background-color 0.3s, color 0.3s; +} + +[data-theme='dark'] .theme-button:hover { + background-color: #0f5f74; +} + +[data-theme='light'] .theme-button:hover { + bbackground-color: #145d7b; +} + +select { + border-radius: 30px; + padding: 0.125rem 0.25rem; + background-color: var(--tertiary-background-color); + color: var(--secondary-text-color); + border: none; +} + +label { + margin: 0; +} + +#eventDisplay { + height: 100vh; + overflow: hidden; +} + +.form-control, +.form-control:focus { + color: var(--text-color); + background-color: var(--tertiary-background-color); + border: 1px solid var(--border-color); +} + + + From a1311f1db00aed2024e0e590afd2dabe21cb4c22 Mon Sep 17 00:00:00 2001 From: "sqrd.max" Date: Wed, 30 Oct 2024 02:04:20 +0200 Subject: [PATCH 11/14] Try to make theming work --- .../display-shell/display-shell.component.scss | 3 --- .../event-selector/event-selector.component.scss | 6 +++--- .../menu-toggle/menu-toggle.component.scss | 16 ++++++++-------- .../main-display/main-display.component.scss | 1 + .../pages/main-display/main-display.component.ts | 1 + 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/firebird-ng/src/app/components/display-shell/display-shell.component.scss b/firebird-ng/src/app/components/display-shell/display-shell.component.scss index c00a782..8dbc652 100644 --- a/firebird-ng/src/app/components/display-shell/display-shell.component.scss +++ b/firebird-ng/src/app/components/display-shell/display-shell.component.scss @@ -14,8 +14,6 @@ flex-direction: row; align-items: center; min-height: 50px; - //background-color: #2e2e2e; - border-bottom: 1px solid #1c1c1c; box-sizing: border-box; } @@ -41,7 +39,6 @@ .central-pane { flex: 1; - //background-color: #424242; overflow: auto; } diff --git a/firebird-ng/src/app/components/event-selector/event-selector.component.scss b/firebird-ng/src/app/components/event-selector/event-selector.component.scss index e1afa73..4faec41 100644 --- a/firebird-ng/src/app/components/event-selector/event-selector.component.scss +++ b/firebird-ng/src/app/components/event-selector/event-selector.component.scss @@ -4,8 +4,8 @@ padding: 5px 10px; font-size: 12px; border: 1px solid rgba(88, 88, 88, 0.08); - box-shadow: var(--phoenix-icon-shadow); - background-color: var(--phoenix-background-color-tertiary); - color: var(--phoenix-text-color-secondary); + box-shadow: var(--icon-shadow); + background-color: var(--tertiary-background-color); + color: var(--secondary-text-color); } } diff --git a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss index 2727a9b..bb48b83 100644 --- a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss +++ b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss @@ -37,11 +37,11 @@ } } } -.menu-toggle-icon{ - color: var(--text-color); - transition: background-color 0.3s, color 0.3s; -} - -.button_theme:hover { - color: var(--accent-color); -} +//.menu-toggle-icon{ +// color: var(--text-color); +// transition: background-color 0.3s, color 0.3s; +//} +// +//.button_theme:hover { +// color: var(--accent-color); +//} diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.scss b/firebird-ng/src/app/pages/main-display/main-display.component.scss index 72ea135..cbba160 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.scss +++ b/firebird-ng/src/app/pages/main-display/main-display.component.scss @@ -10,6 +10,7 @@ display: flex; align-items: center; justify-content: center; + color: var(--text-color); //background-color: #2e2e2e; flex-wrap: wrap; } diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.ts b/firebird-ng/src/app/pages/main-display/main-display.component.ts index 89c8f1b..62a7dbb 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.ts +++ b/firebird-ng/src/app/pages/main-display/main-display.component.ts @@ -111,6 +111,7 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { private beamAnimationTime: number = 1000; isLeftPaneOpen: boolean = false; + isDarkTheme = false; isPhoenixMenuOpen: boolean = false; isSmallScreen: boolean = window.innerWidth < 768; From 8b082c863109ea81bd898a5966fa3515fe70e1c1 Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Tue, 29 Oct 2024 20:46:12 -0400 Subject: [PATCH 12/14] Theme works again --- .../auto-rotate/auto-rotate.component.html | 2 +- .../dark-theme/dark-theme.component.html | 2 +- .../dark-theme/dark-theme.component.ts | 14 ++- .../main-view-toggle.component.html | 8 -- .../main-view-toggle.component.scss | 0 .../main-view-toggle.component.test.ts | 49 ---------- .../main-view-toggle.component.ts | 28 ------ .../menu-toggle/menu-toggle.component.html | 9 +- .../menu-toggle/menu-toggle.component.scss | 98 +++++++++++-------- .../menu-toggle/menu-toggle.component.ts | 6 +- .../object-clipping.component.html | 2 +- .../tool-panel/tool-panel.component.html | 6 +- .../tool-panel/tool-panel.component.scss | 2 +- .../tool-panel/tool-panel.component.ts | 12 ++- .../view-options/view-options.component.html | 19 ++-- .../view-options/view-options.component.scss | 22 +++++ .../view-options/view-options.component.ts | 4 +- 17 files changed, 132 insertions(+), 151 deletions(-) delete mode 100644 firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.html delete mode 100644 firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.scss delete mode 100644 firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.test.ts delete mode 100644 firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.ts diff --git a/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.html b/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.html index cb3eb95..0af25be 100644 --- a/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.html +++ b/firebird-ng/src/app/components/auto-rotate/auto-rotate.component.html @@ -1,6 +1,6 @@ diff --git a/firebird-ng/src/app/components/dark-theme/dark-theme.component.html b/firebird-ng/src/app/components/dark-theme/dark-theme.component.html index 5ab48a9..1a7a068 100644 --- a/firebird-ng/src/app/components/dark-theme/dark-theme.component.html +++ b/firebird-ng/src/app/components/dark-theme/dark-theme.component.html @@ -1,7 +1,7 @@ diff --git a/firebird-ng/src/app/components/dark-theme/dark-theme.component.ts b/firebird-ng/src/app/components/dark-theme/dark-theme.component.ts index 051984e..a5c92b4 100644 --- a/firebird-ng/src/app/components/dark-theme/dark-theme.component.ts +++ b/firebird-ng/src/app/components/dark-theme/dark-theme.component.ts @@ -1,6 +1,7 @@ import { Component, type OnInit } from '@angular/core'; import {EventDisplayService} from "phoenix-ui-components"; import {MenuToggleComponent} from "../menu-toggle/menu-toggle.component"; +import * as THREE from 'three'; @Component({ selector: 'app-custom-dark-theme', @@ -13,8 +14,10 @@ import {MenuToggleComponent} from "../menu-toggle/menu-toggle.component"; }) export class DarkThemeComponent implements OnInit { darkTheme = false; + threeDarkBackground = new THREE.Color( 0x3F3F3F ); + threeLightBackground = new THREE.Color( 0xF3F3F3 ); - constructor(private eventDisplay: EventDisplayService) {} + constructor(private eventDisplay: EventDisplayService) { } ngOnInit(): void { this.darkTheme = this.eventDisplay.getUIManager().getDarkTheme(); @@ -22,6 +25,13 @@ export class DarkThemeComponent implements OnInit { setDarkTheme() { this.darkTheme = !this.darkTheme; - this.eventDisplay.getUIManager().setDarkTheme(this.darkTheme); + const scene = this.eventDisplay.getThreeManager().getSceneManager().getScene(); + + // Switch three.js background + if(scene && this.darkTheme) { + scene.background = this.threeDarkBackground; + } else { + scene.background = this.threeLightBackground; + } } } diff --git a/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.html b/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.html deleted file mode 100644 index 66aeb51..0000000 --- a/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.html +++ /dev/null @@ -1,8 +0,0 @@ - - diff --git a/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.scss b/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.scss deleted file mode 100644 index e69de29..0000000 diff --git a/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.test.ts b/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.test.ts deleted file mode 100644 index ce19eda..0000000 --- a/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { MainViewToggleComponent } from './main-view-toggle.component'; -import { EventDisplayService } from '../../../services/event-display.service'; -import { PhoenixUIModule } from '../../phoenix-ui.module'; - -describe('MainViewToggleComponent', () => { - let component: MainViewToggleComponent; - let fixture: ComponentFixture; - - const mockEventDisplay = { - getUIManager: jest.fn().mockReturnThis(), - toggleOrthographicView: jest.fn().mockReturnThis(), - }; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [PhoenixUIModule], - providers: [ - { - provide: EventDisplayService, - useValue: mockEventDisplay, - }, - ], - declarations: [MainViewToggleComponent], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(MainViewToggleComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should switch main view', () => { - expect(component.orthographicView).toBe(false); - - component.switchMainView(); - - expect(component.orthographicView).toBe(true); - expect( - mockEventDisplay.getUIManager().toggleOrthographicView, - ).toHaveBeenCalled(); - }); -}); diff --git a/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.ts b/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.ts deleted file mode 100644 index 12aa669..0000000 --- a/firebird-ng/src/app/components/main-view-toggle/main-view-toggle.component.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Component } from '@angular/core'; -import {EventDisplayService} from "phoenix-ui-components"; -import {MenuToggleComponent} from "../menu-toggle/menu-toggle.component"; -import {MatTooltip} from "@angular/material/tooltip"; - - -@Component({ - selector: 'app-custom-main-view-toggle', - templateUrl: './main-view-toggle.component.html', - styleUrls: ['./main-view-toggle.component.scss'], - imports: [ - MenuToggleComponent, - MatTooltip - ], - standalone: true -}) -export class MainViewToggleComponent { - orthographicView: boolean = false; - - constructor(private eventDisplay: EventDisplayService) {} - - switchMainView() { - this.orthographicView = !this.orthographicView; - this.eventDisplay - .getUIManager() - .toggleOrthographicView(this.orthographicView); - } -} diff --git a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.html b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.html index fcdd471..867a57c 100644 --- a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.html +++ b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.html @@ -1,11 +1,12 @@ diff --git a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss index 2727a9b..a532fb2 100644 --- a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss +++ b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.scss @@ -1,47 +1,59 @@ -:host { - display: flex; - margin: 0 0.6rem; - .menu-toggle { - display: flex; - background: unset; - border: none; - height: 2.5rem; - width: 2.5rem; - min-height: 2.5rem; - min-width: 2.5rem; - padding: 0.65rem; - cursor: pointer; - align-self: center; - transition: all 0.4s; - - &-icon { - width: 100%; - height: 100%; - fill: var(--icon-color); - - &.active-icon { - fill: var(--active-icon-color); - } - } +.toggle-button { + width: 40px; + align-content: center; + min-width: 40px; + padding-left: 13px; + padding-right: 0; + margin-left: 5px; +} - &:hover { - background-color: var(--icon-bg); - border-radius: 40%; - transition: all 0.4s; - } - &.disabled { - cursor: not-allowed; - opacity: 0.4; - } - } -} -.menu-toggle-icon{ - color: var(--text-color); - transition: background-color 0.3s, color 0.3s; -} -.button_theme:hover { - color: var(--accent-color); -} +//:host { +// display: flex; +// margin: 0 0.6rem; +// +// .menu-toggle { +// display: flex; +// background: unset; +// border: none; +// height: 2.5rem; +// width: 2.5rem; +// min-height: 2.5rem; +// min-width: 2.5rem; +// padding: 0.65rem; +// cursor: pointer; +// align-self: center; +// transition: all 0.4s; +// +// &-icon { +// width: 100%; +// height: 100%; +// fill: var(--icon-color); +// +// &.active-icon { +// fill: var(--active-icon-color); +// } +// } +// +// &:hover { +// background-color: var(--icon-bg); +// border-radius: 40%; +// transition: all 0.4s; +// } +// +// &.disabled { +// cursor: not-allowed; +// opacity: 0.4; +// } +// } +//} +//.menu-toggle-icon{ +// color: var(--text-color); +// transition: background-color 0.3s, color 0.3s; +//} +// +//.button_theme:hover { +// color: var(--accent-color); +//} diff --git a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.ts b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.ts index e2b8b83..3995237 100644 --- a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.ts +++ b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.ts @@ -1,6 +1,8 @@ import { Component, Input } from '@angular/core'; import {NgClass} from "@angular/common"; import {MatTooltip} from "@angular/material/tooltip"; +import {MatButton} from "@angular/material/button"; +import {MatIcon} from "@angular/material/icon"; @Component({ selector: 'app-custom-menu-toggle', @@ -8,7 +10,9 @@ import {MatTooltip} from "@angular/material/tooltip"; styleUrls: ['./menu-toggle.component.scss'], imports: [ NgClass, - MatTooltip + MatTooltip, + MatButton, + MatIcon ], standalone: true }) diff --git a/firebird-ng/src/app/components/object-clipping/object-clipping.component.html b/firebird-ng/src/app/components/object-clipping/object-clipping.component.html index 6f154ac..01fcc23 100644 --- a/firebird-ng/src/app/components/object-clipping/object-clipping.component.html +++ b/firebird-ng/src/app/components/object-clipping/object-clipping.component.html @@ -46,7 +46,7 @@ diff --git a/firebird-ng/src/app/components/tool-panel/tool-panel.component.html b/firebird-ng/src/app/components/tool-panel/tool-panel.component.html index cab3b4c..e33205d 100644 --- a/firebird-ng/src/app/components/tool-panel/tool-panel.component.html +++ b/firebird-ng/src/app/components/tool-panel/tool-panel.component.html @@ -22,7 +22,11 @@ zoom_out - + +
diff --git a/firebird-ng/src/app/components/tool-panel/tool-panel.component.scss b/firebird-ng/src/app/components/tool-panel/tool-panel.component.scss index 0e87eee..de78843 100644 --- a/firebird-ng/src/app/components/tool-panel/tool-panel.component.scss +++ b/firebird-ng/src/app/components/tool-panel/tool-panel.component.scss @@ -25,7 +25,7 @@ transform: translateX(calc(100% - 60px)); } -.icon-button { +.tool-panel button { background: none; border: none; cursor: pointer; diff --git a/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts b/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts index 50d49d5..ff29018 100644 --- a/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts +++ b/firebird-ng/src/app/components/tool-panel/tool-panel.component.ts @@ -4,7 +4,6 @@ import {MatIcon} from "@angular/material/icon"; import {EventDisplayService, PhoenixUIModule} from 'phoenix-ui-components'; import {PhoenixThreeFacade} from "../../utils/phoenix-three-facade"; import {ViewOptionsComponent} from "../view-options/view-options.component"; -import {MainViewToggleComponent} from "../main-view-toggle/main-view-toggle.component"; @Component({ selector: 'app-tool-panel', @@ -13,8 +12,7 @@ import {MainViewToggleComponent} from "../main-view-toggle/main-view-toggle.comp NgIf, MatIcon, PhoenixUIModule, - ViewOptionsComponent, - MainViewToggleComponent + ViewOptionsComponent ], templateUrl: './tool-panel.component.html', styleUrl: './tool-panel.component.scss' @@ -29,6 +27,7 @@ export class ToolPanelComponent { private zoomTimeout: any; /** The speed and time of zoom. */ private zoomTime: number = 100; + private orthographicView: boolean = false; constructor( private eventDisplay: EventDisplayService) @@ -74,6 +73,13 @@ export class ToolPanelComponent { this.isCollapsed = !this.isCollapsed; } + switchMainView() { + this.orthographicView = !this.orthographicView; + this.eventDisplay + .getUIManager() + .toggleOrthographicView(this.orthographicView); + } + } diff --git a/firebird-ng/src/app/components/view-options/view-options.component.html b/firebird-ng/src/app/components/view-options/view-options.component.html index 4619b3b..5afbc9d 100644 --- a/firebird-ng/src/app/components/view-options/view-options.component.html +++ b/firebird-ng/src/app/components/view-options/view-options.component.html @@ -94,10 +94,15 @@ - - + + + + + + + + + diff --git a/firebird-ng/src/app/components/view-options/view-options.component.scss b/firebird-ng/src/app/components/view-options/view-options.component.scss index 980a33d..dc77d0a 100644 --- a/firebird-ng/src/app/components/view-options/view-options.component.scss +++ b/firebird-ng/src/app/components/view-options/view-options.component.scss @@ -1,3 +1,25 @@ +button { + background: none; + border: none; + cursor: pointer; + padding: 10px; + display: flex; + justify-content: center; + align-items: center; + margin-left: 7px; + border-radius: 50%; + transition: background-color 0.3s ease; + + &:hover { + background-color: #5c5c5c; + } + + mat-icon { + font-size: 24px; + color: white; + } +} + .view-icon { width: 1.2rem; height: 1.2rem; diff --git a/firebird-ng/src/app/components/view-options/view-options.component.ts b/firebird-ng/src/app/components/view-options/view-options.component.ts index eb5eaee..6bbe5ca 100644 --- a/firebird-ng/src/app/components/view-options/view-options.component.ts +++ b/firebird-ng/src/app/components/view-options/view-options.component.ts @@ -14,6 +14,7 @@ import { Vector3 } from 'three'; import {MatMenu, MatMenuItem, MatMenuTrigger} from '@angular/material/menu'; import {NgForOf, NgIf} from "@angular/common"; import {MenuToggleComponent} from "../menu-toggle/menu-toggle.component"; +import {MatIcon} from "@angular/material/icon"; @Component({ selector: 'app-custom-view-options', @@ -26,7 +27,8 @@ import {MenuToggleComponent} from "../menu-toggle/menu-toggle.component"; NgForOf, MenuToggleComponent, MatMenuTrigger, - NgIf + NgIf, + MatIcon ], standalone: true }) From e013aedf2c52f8b32170271efc368b610e70e219 Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Tue, 29 Oct 2024 21:05:33 -0400 Subject: [PATCH 13/14] Fix styles for theme switching --- .../src/app/components/dark-theme/dark-theme.component.html | 1 + .../src/app/components/dark-theme/dark-theme.component.ts | 5 +++++ firebird-ng/src/styles.scss | 6 +++--- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/firebird-ng/src/app/components/dark-theme/dark-theme.component.html b/firebird-ng/src/app/components/dark-theme/dark-theme.component.html index 1a7a068..6b7df62 100644 --- a/firebird-ng/src/app/components/dark-theme/dark-theme.component.html +++ b/firebird-ng/src/app/components/dark-theme/dark-theme.component.html @@ -3,5 +3,6 @@ [active]="darkTheme" icon="brightness_6" (click)="setDarkTheme()" + class="theme-button" > diff --git a/firebird-ng/src/app/components/dark-theme/dark-theme.component.ts b/firebird-ng/src/app/components/dark-theme/dark-theme.component.ts index a5c92b4..45dc10d 100644 --- a/firebird-ng/src/app/components/dark-theme/dark-theme.component.ts +++ b/firebird-ng/src/app/components/dark-theme/dark-theme.component.ts @@ -27,11 +27,16 @@ export class DarkThemeComponent implements OnInit { this.darkTheme = !this.darkTheme; const scene = this.eventDisplay.getThreeManager().getSceneManager().getScene(); + this.eventDisplay.getUIManager().setDarkTheme(this.darkTheme); + // Switch three.js background if(scene && this.darkTheme) { scene.background = this.threeDarkBackground; } else { scene.background = this.threeLightBackground; } + + const theme = this.darkTheme ? 'dark' : 'light'; + document.documentElement.setAttribute('data-theme', theme); } } diff --git a/firebird-ng/src/styles.scss b/firebird-ng/src/styles.scss index dc08080..e8aa62b 100644 --- a/firebird-ng/src/styles.scss +++ b/firebird-ng/src/styles.scss @@ -49,7 +49,7 @@ $light-theme: mat.define-light-theme($colors); --dat-border-color: #f0f0f0; } -/* Тёмная тема */ +/* Dark theme */ [data-theme='dark'] { --background-color: #2e2e2e; --secondary-background-color: #292929; @@ -89,7 +89,7 @@ body { background-color: var(--background-color); } -/* Стили скроллбара */ +/* Scroll bar styles */ ::-webkit-scrollbar { width: 0.3em; height: 0.3em; @@ -136,7 +136,7 @@ body { } [data-theme='light'] .theme-button:hover { - bbackground-color: #145d7b; + background-color: #145d7b; } select { From 6462dc32118bd059f153687f3299d9d80c94976a Mon Sep 17 00:00:00 2001 From: Dmitry Romanov Date: Tue, 29 Oct 2024 23:25:40 -0400 Subject: [PATCH 14/14] Fix unit tests --- .../dark-theme/dark-theme.component.test.ts | 51 ----------------- .../event-selector.component.test.ts | 56 ------------------- .../menu-toggle/menu-toggle.component.test.ts | 25 --------- .../nav-config/nav-config.component.spec.ts | 23 -------- .../main-display/main-display.component.ts | 40 +------------ .../src/app/services/ui-theme.service.ts | 0 6 files changed, 2 insertions(+), 193 deletions(-) delete mode 100644 firebird-ng/src/app/components/dark-theme/dark-theme.component.test.ts delete mode 100644 firebird-ng/src/app/components/event-selector/event-selector.component.test.ts delete mode 100644 firebird-ng/src/app/components/menu-toggle/menu-toggle.component.test.ts delete mode 100644 firebird-ng/src/app/components/nav-config/nav-config.component.spec.ts create mode 100644 firebird-ng/src/app/services/ui-theme.service.ts diff --git a/firebird-ng/src/app/components/dark-theme/dark-theme.component.test.ts b/firebird-ng/src/app/components/dark-theme/dark-theme.component.test.ts deleted file mode 100644 index 2e3187c..0000000 --- a/firebird-ng/src/app/components/dark-theme/dark-theme.component.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { DarkThemeComponent } from './dark-theme.component'; -import { EventDisplayService } from '../../../services/event-display.service'; -import { PhoenixUIModule } from '../../phoenix-ui.module'; - -describe('DarkThemeComponent', () => { - let component: DarkThemeComponent; - let fixture: ComponentFixture; - - const mockEventDisplay = { - getUIManager: jest.fn().mockReturnThis(), - getDarkTheme: jest.fn().mockReturnThis(), - setDarkTheme: jest.fn().mockReturnThis(), - }; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [PhoenixUIModule], - providers: [ - { - provide: EventDisplayService, - useValue: mockEventDisplay, - }, - ], - declarations: [DarkThemeComponent], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(DarkThemeComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should initially get dark theme', () => { - jest.spyOn(mockEventDisplay, 'getDarkTheme'); - component.ngOnInit(); - expect(mockEventDisplay.getDarkTheme).toHaveBeenCalled(); - }); - - it('should set/toggle dark theme', () => { - component.darkTheme = false; - component.setDarkTheme(); - expect(component.darkTheme).toBe(true); - }); -}); diff --git a/firebird-ng/src/app/components/event-selector/event-selector.component.test.ts b/firebird-ng/src/app/components/event-selector/event-selector.component.test.ts deleted file mode 100644 index 4ebd7c4..0000000 --- a/firebird-ng/src/app/components/event-selector/event-selector.component.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { EventSelectorComponent } from './event-selector.component'; -import { EventDisplayService } from '../../../services/event-display.service'; -import { PhoenixUIModule } from '../../phoenix-ui.module'; - -describe('EventSelectorComponent', () => { - let component: EventSelectorComponent; - let fixture: ComponentFixture; - - const mockEventDisplayService = { - listenToLoadedEventsChange: jest.fn(), - loadEvent: jest.fn(), - }; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [PhoenixUIModule], - providers: [ - { - provide: EventDisplayService, - useValue: mockEventDisplayService, - }, - ], - declarations: [EventSelectorComponent], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(EventSelectorComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should initialize to listen to loaded events change', () => { - component.ngOnInit(); - - expect( - mockEventDisplayService.listenToLoadedEventsChange, - ).toHaveBeenCalled(); - }); - - it('should change event through event display', () => { - const mockSelectEvent = { target: { value: 'TestEvent' } }; - - component.changeEvent(mockSelectEvent); - - expect(mockEventDisplayService.loadEvent).toHaveBeenCalledWith( - mockSelectEvent.target.value, - ); - }); -}); diff --git a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.test.ts b/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.test.ts deleted file mode 100644 index 570bdf8..0000000 --- a/firebird-ng/src/app/components/menu-toggle/menu-toggle.component.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { PhoenixUIModule } from '../../phoenix-ui.module'; - -import { MenuToggleComponent } from './menu-toggle.component'; - -describe('MenuToggleComponent', () => { - let component: MenuToggleComponent; - let fixture: ComponentFixture; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [PhoenixUIModule], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(MenuToggleComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/firebird-ng/src/app/components/nav-config/nav-config.component.spec.ts b/firebird-ng/src/app/components/nav-config/nav-config.component.spec.ts deleted file mode 100644 index 2ff396d..0000000 --- a/firebird-ng/src/app/components/nav-config/nav-config.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { NavConfigComponent } from './nav-config.component'; - -describe('NavConfigComponent', () => { - let component: NavConfigComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [NavConfigComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(NavConfigComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/firebird-ng/src/app/pages/main-display/main-display.component.ts b/firebird-ng/src/app/pages/main-display/main-display.component.ts index 62a7dbb..5c57163 100644 --- a/firebird-ng/src/app/pages/main-display/main-display.component.ts +++ b/firebird-ng/src/app/pages/main-display/main-display.component.ts @@ -1,6 +1,4 @@ -import {AfterViewInit, Component, ElementRef, HostListener, Input, OnInit, Renderer2, ViewChild} from '@angular/core'; -import {HttpClient, HttpClientModule} from '@angular/common/http'; - +import {AfterViewInit, Component, HostListener, Input, OnInit, ViewChild} from '@angular/core'; import { EventDataFormat, EventDataImportOption, @@ -10,24 +8,12 @@ import {ClippingSetting, Configuration, PhoenixLoader, PhoenixMenuNode, PresetVi import * as THREE from 'three'; import {Color, DoubleSide, InstancedBufferGeometry, Line, MeshLambertMaterial, MeshPhongMaterial,} from "three"; import {ALL_GROUPS, GeometryService} from '../../services/geometry.service'; -import {ActivatedRoute, RouterLink, RouterOutlet} from '@angular/router'; import {ThreeGeometryProcessor} from "../../data-pipelines/three-geometry.processor"; import * as TWEEN from '@tweenjs/tween.js'; -import GUI from "lil-gui"; import {produceRenderOrder} from "jsroot/geom"; -import { - disposeHierarchy, - disposeNode, - findObject3DNodes, - getColorOrDefault, - pruneEmptyNodes -} from "../../utils/three.utils"; +import {getColorOrDefault} from "../../utils/three.utils"; import {PhoenixThreeFacade} from "../../utils/phoenix-three-facade"; -import {BehaviorSubject, Subject} from "rxjs"; import {GameControllerService} from "../../services/game-controller.service"; -import {LineMaterial} from "three/examples/jsm/lines/LineMaterial"; -import {Line2} from "three/examples/jsm/lines/Line2"; -import {LineGeometry} from "three/examples/jsm/lines/LineGeometry"; import {IoOptionsComponent} from "../../components/io-options/io-options.component"; import {ProcessTrackInfo, ThreeEventProcessor} from "../../data-pipelines/three-event.processor"; import {UserConfigService} from "../../services/user-config.service"; @@ -45,7 +31,6 @@ import {AngularSplitModule} from "angular-split"; import {SceneTreeComponent} from "../geometry-tree/scene-tree.component"; import {DisplayShellComponent} from "../../components/display-shell/display-shell.component"; import {DataModelPainter} from "../../painters/data-model-painter"; -import {AppComponent} from "../../app.component"; import {ToolPanelComponent} from "../../components/tool-panel/tool-panel.component"; import {NavConfigComponent} from "../../components/nav-config/nav-config.component"; import {UrlService} from "../../services/url.service"; @@ -55,9 +40,6 @@ import {DarkThemeComponent} from "../../components/dark-theme/dark-theme.compone import {ObjectClippingComponent} from "../../components/object-clipping/object-clipping.component"; -// import { LineMaterial } from 'three/addons/lines/LineMaterial.js'; - - @Component({ selector: 'app-test-experiment', templateUrl: './main-display.component.html', @@ -123,11 +105,8 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { private geomService: GeometryService, private eventDisplay: EventDisplayService, private controller: GameControllerService, - private route: ActivatedRoute, private settings: UserConfigService, private dataService: DataModelService, - private elRef: ElementRef, - private renderer2: Renderer2, private urlService: UrlService, private _snackBar: MatSnackBar) { this.threeFacade = new PhoenixThreeFacade(this.eventDisplay); @@ -622,22 +601,7 @@ export class MainDisplayComponent implements OnInit, AfterViewInit { threeManager.setAnimationLoop(()=>{this.handleGamepadInputV1()}); - - //const events_url = "https://eic.github.io/epic/artifacts/sim_dis_10x100_minQ2=1000_epic_craterlake.edm4hep.root/sim_dis_10x100_minQ2=1000_epic_craterlake.edm4hep.root" - //const events_url = "https://eic.github.io/epic/artifacts/sim_dis_10x100_minQ2=1000_epic_craterlake.edm4hep.root" - // const events_url = "assets/events/sim_dis_10x100_minQ2=1000_epic_craterlake.edm4hep.root" - // let loader = new Edm4hepRootEventLoader(); - // loader.openFile(events_url).then(value => { - // console.log('Opened root file'); - // } - // ); - - const geometryAddress = this.route.snapshot.queryParams['geo']; - console.log(`geometry query: ${geometryAddress}`); - - let jsonGeometry; this.loadGeometry().then(jsonGeom => { - jsonGeometry = jsonGeom; this.updateSceneTreeComponent(); diff --git a/firebird-ng/src/app/services/ui-theme.service.ts b/firebird-ng/src/app/services/ui-theme.service.ts new file mode 100644 index 0000000..e69de29