From 74ac7bd7ccf7499959d300003ef73a684d0d3d6d Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 1 Feb 2024 00:41:42 +0100 Subject: [PATCH] implemented selection size rendering, fixes #310 --- CHANGELOG.md | 6 ++ .../captions/captions.component.html | 8 ++- .../captions/captions.component.scss | 4 +- .../components/captions/captions.component.ts | 30 +++++--- .../tile-selector/tile-selector.scene.ts | 6 +- .../dialogs/settings/settings.component.html | 30 ++++++-- .../dialogs/settings/settings.component.ts | 52 ++++++++------ .../app/components/phaser/phaser.component.ts | 5 +- .../editor/event-editor.component.ts | 2 +- .../src/app/services/global-events.service.ts | 1 + webapp/src/app/services/globals.ts | 2 + .../src/app/services/http-client.service.ts | 2 +- .../app/services/phaser/entities/cc-entity.ts | 1 + .../services/phaser/tilemap/tile-drawer.ts | 67 +++++++++++++++--- webapp/src/app/services/settings.service.ts | 44 +++++++----- webapp/src/assets/selection-dark.png | Bin 0 -> 6820 bytes webapp/src/assets/selection-light.png | Bin 0 -> 6788 bytes 17 files changed, 192 insertions(+), 68 deletions(-) create mode 100644 webapp/src/assets/selection-dark.png create mode 100644 webapp/src/assets/selection-light.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b772862..259c0906 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Added +- Render dimensions when selecting multiple tiles [#310](https://github.com/CCDirectLink/crosscode-map-editor/issues/310) + +### Changed +- Increased font resolution for entity names + ## [1.2.0] 2024-01-30 ### Added - Toggle in settings that also shows the vanilla maps in the map selection menu diff --git a/webapp/src/app/components/captions/captions.component.html b/webapp/src/app/components/captions/captions.component.html index 88819bf9..0b8ff06f 100644 --- a/webapp/src/app/components/captions/captions.component.html +++ b/webapp/src/app/components/captions/captions.component.html @@ -1,2 +1,8 @@

{{ version }}

-

{{ coords }}

+

+ + + {{el.text}} + + +

diff --git a/webapp/src/app/components/captions/captions.component.scss b/webapp/src/app/components/captions/captions.component.scss index 3e845772..8c76c985 100644 --- a/webapp/src/app/components/captions/captions.component.scss +++ b/webapp/src/app/components/captions/captions.component.scss @@ -13,10 +13,10 @@ right: 0; } -.coords { +.bottom-elements { background-color: #0005; - &.inactive { + &:empty { display: none; } } diff --git a/webapp/src/app/components/captions/captions.component.ts b/webapp/src/app/components/captions/captions.component.ts index 6ec461cf..39f901aa 100644 --- a/webapp/src/app/components/captions/captions.component.ts +++ b/webapp/src/app/components/captions/captions.component.ts @@ -2,6 +2,11 @@ import { Component, OnInit } from '@angular/core'; import { environment } from '../../../environments/environment'; import { Globals } from '../../services/globals'; +export interface BottomUiElement { + text?: string; + active?: boolean; +} + @Component({ selector: 'app-captions', templateUrl: './captions.component.html', @@ -9,16 +14,23 @@ import { Globals } from '../../services/globals'; }) export class CaptionsComponent implements OnInit { version = environment.version; - coords = ''; - coordsClass = 'inactive'; - + coords: BottomUiElement = {}; + selectionSize: BottomUiElement = {}; + + uiElements: BottomUiElement[] = [ + this.coords, + this.selectionSize + ]; + ngOnInit(): void { - Globals.globalEventsService.updateCoords.subscribe((coords) => { - this.coords = !coords - ? '' - : `(${coords.x}, ${coords.y}, ${coords.z})`; - - this.coordsClass = coords ? '' : 'inactive'; + Globals.globalEventsService.updateCoords.subscribe(coords => { + this.coords.text = `(${coords?.x}, ${coords?.y}, ${coords?.z})`; + this.coords.active = !!coords; + }); + + Globals.globalEventsService.updateTileSelectionSize.subscribe(size => { + this.selectionSize.text = `${size?.x}x${size?.y}`; + this.selectionSize.active = !!size; }); } } diff --git a/webapp/src/app/components/dialogs/floating-window/tile-selector/tile-selector.scene.ts b/webapp/src/app/components/dialogs/floating-window/tile-selector/tile-selector.scene.ts index 5339b75b..4589a4c2 100644 --- a/webapp/src/app/components/dialogs/floating-window/tile-selector/tile-selector.scene.ts +++ b/webapp/src/app/components/dialogs/floating-window/tile-selector/tile-selector.scene.ts @@ -237,6 +237,10 @@ export class TileSelectorScene extends Phaser.Scene { } this.rect = this.add.rectangle(x * Globals.TILE_SIZE, y * Globals.TILE_SIZE, width * Globals.TILE_SIZE, height * Globals.TILE_SIZE); this.rect.setOrigin(0, 0); - this.rect.setStrokeStyle(1, 0xffffff, 0.6); + if (Globals.settingsService.getSettings().selectionBoxDark) { + this.rect.setStrokeStyle(2, 0x333333, 0.9); + } else { + this.rect.setStrokeStyle(2, 0xffffff, 0.6); + } } } diff --git a/webapp/src/app/components/dialogs/settings/settings.component.html b/webapp/src/app/components/dialogs/settings/settings.component.html index 81219f6c..1d861f24 100644 --- a/webapp/src/app/components/dialogs/settings/settings.component.html +++ b/webapp/src/app/components/dialogs/settings/settings.component.html @@ -15,7 +15,7 @@ @@ -24,21 +24,43 @@ Mod None - {{mod}} + {{ mod }} Maps will be stored and loaded from the selected mod
- + Include vanilla maps
- + Wrap event editor lines
+
+

Selection Box Style

+
+ + +
+
diff --git a/webapp/src/app/components/dialogs/settings/settings.component.ts b/webapp/src/app/components/dialogs/settings/settings.component.ts index 4305f06d..babb79ee 100644 --- a/webapp/src/app/components/dialogs/settings/settings.component.ts +++ b/webapp/src/app/components/dialogs/settings/settings.component.ts @@ -6,9 +6,10 @@ import { BrowserService } from '../../../services/browser.service'; import { ElectronService } from '../../../services/electron.service'; import { Globals } from '../../../services/globals'; import { HttpClientService } from '../../../services/http-client.service'; -import { SettingsService } from '../../../services/settings.service'; +import { AppSettings, SettingsService } from '../../../services/settings.service'; import { SharedService } from '../../../services/shared-service'; import { OverlayRefControl } from '../overlay/overlay-ref-control'; +import { PropListCard } from '../../widgets/shared/image-select-overlay/image-select-card/image-select-card.component'; @Component({ selector: 'app-settings', @@ -16,19 +17,28 @@ import { OverlayRefControl } from '../overlay/overlay-ref-control'; styleUrls: ['./settings.component.scss'] }) export class SettingsComponent implements OnInit { - + isElectron = Globals.isElectron; folderFormControl = new FormControl(); icon = 'help_outline'; iconCss = 'icon-undefined'; mods: string[] = []; mod = ''; - wrapEventEditorLines: boolean; - includeVanillaMaps: boolean; + settings: AppSettings; isIncludeVanillaMapsDisabled: boolean; - + + cardLight: PropListCard = { + name: 'Light', + imgSrc: 'assets/selection-light.png', + }; + + cardDark: PropListCard = { + name: 'Dark', + imgSrc: 'assets/selection-dark.png', + }; + private readonly sharedService: SharedService; - + constructor( private ref: OverlayRefControl, private electron: ElectronService, @@ -42,28 +52,27 @@ export class SettingsComponent implements OnInit { } else { this.sharedService = browser; } - + http.getMods().subscribe(mods => this.mods = mods); this.mod = this.sharedService.getSelectedMod(); this.isIncludeVanillaMapsDisabled = !this.mod; - this.wrapEventEditorLines = this.settingsService.wrapEventEditorLines; - this.includeVanillaMaps = this.settingsService.includeVanillaMaps; + this.settings = JSON.parse(JSON.stringify(this.settingsService.getSettings())); } - + ngOnInit() { if (this.isElectron) { this.folderFormControl.setValue(this.electron.getAssetsPath()); this.folderFormControl.valueChanges.subscribe(() => this.resetIcon()); } - + this.check(); } - + private resetIcon() { this.icon = 'help_outline'; this.iconCss = 'icon-undefined'; } - + private setIcon(valid: boolean) { if (valid) { this.icon = 'check'; @@ -73,14 +82,14 @@ export class SettingsComponent implements OnInit { this.iconCss = 'icon-invalid'; } } - + select() { const path = this.electron.selectCcFolder(); if (path) { this.folderFormControl.setValue(path); } } - + check() { const valid = this.electron.checkAssetsPath(this.folderFormControl.value); this.setIcon(valid); @@ -92,28 +101,27 @@ export class SettingsComponent implements OnInit { }); } } - + modSelectEvent(selectedMod: string) { this.isIncludeVanillaMapsDisabled = !selectedMod; } - + save() { if (this.isElectron) { this.electron.saveAssetsPath(this.folderFormControl.value); } this.sharedService.saveModSelect(this.mod); - this.settingsService.wrapEventEditorLines = this.wrapEventEditorLines; - this.settingsService.includeVanillaMaps = this.includeVanillaMaps; + this.settingsService.updateSettings(this.settings); this.close(); const ref = this.snackBar.open('Changing the path requires to restart the editor', 'Restart', { duration: 6000 }); - + ref.onAction().subscribe(() => this.sharedService.relaunch()); } - + close() { this.ref.close(); } - + } diff --git a/webapp/src/app/components/phaser/phaser.component.ts b/webapp/src/app/components/phaser/phaser.component.ts index 2043365e..62cb566c 100644 --- a/webapp/src/app/components/phaser/phaser.component.ts +++ b/webapp/src/app/components/phaser/phaser.component.ts @@ -12,6 +12,7 @@ import { MainScene } from '../../services/phaser/main-scene'; import { PhaserEventsService } from '../../services/phaser/phaser-events.service'; import { StateHistoryService } from '../dialogs/floating-window/history/state-history.service'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { SettingsService } from '../../services/settings.service'; @Component({ selector: 'app-phaser', @@ -32,7 +33,8 @@ export class PhaserComponent implements AfterViewInit { private http: HttpClientService, snackbar: MatSnackBar, registry: EntityRegistryService, - autotile: AutotileService + autotile: AutotileService, + settingsService: SettingsService ) { Globals.stateHistoryService = stateHistory; Globals.mapLoaderService = mapLoader; @@ -42,6 +44,7 @@ export class PhaserComponent implements AfterViewInit { Globals.entityRegistry = registry; Globals.httpService = http; Globals.snackbar = snackbar; + Globals.settingsService = settingsService; } diff --git a/webapp/src/app/components/widgets/event-widget/event-editor/editor/event-editor.component.ts b/webapp/src/app/components/widgets/event-widget/event-editor/editor/event-editor.component.ts index d3a8ae69..932a4956 100644 --- a/webapp/src/app/components/widgets/event-widget/event-editor/editor/event-editor.component.ts +++ b/webapp/src/app/components/widgets/event-widget/event-editor/editor/event-editor.component.ts @@ -65,7 +65,7 @@ export class EventEditorComponent implements OnChanges, OnInit { } ngOnInit() { - this.wrapText = this.settingsService.wrapEventEditorLines; + this.wrapText = this.settingsService.getSettings().wrapEventEditorLines; } ngOnChanges() { diff --git a/webapp/src/app/services/global-events.service.ts b/webapp/src/app/services/global-events.service.ts index 0226dcf6..edc62133 100644 --- a/webapp/src/app/services/global-events.service.ts +++ b/webapp/src/app/services/global-events.service.ts @@ -21,6 +21,7 @@ export class GlobalEventsService { showAddEntityMenu = new Subject(); updateCoords = new Subject(); + updateTileSelectionSize = new Subject(); showIngamePreview = new BehaviorSubject(false); hasUnsavedChanges = new BehaviorSubject(false); diff --git a/webapp/src/app/services/globals.ts b/webapp/src/app/services/globals.ts index 2111ed55..c0a20497 100644 --- a/webapp/src/app/services/globals.ts +++ b/webapp/src/app/services/globals.ts @@ -7,6 +7,7 @@ import { EntityRegistryService } from './phaser/entities/registry/entity-registr import { PhaserEventsService } from './phaser/phaser-events.service'; import { CCMap } from './phaser/tilemap/cc-map'; import { MatSnackBar } from '@angular/material/snack-bar'; +import { SettingsService } from './settings.service'; export class Globals { static isElectron = false; @@ -30,5 +31,6 @@ export class Globals { static autotileService: AutotileService; static entityRegistry: EntityRegistryService; static httpService: HttpClientService; + static settingsService: SettingsService; static snackbar: MatSnackBar; } diff --git a/webapp/src/app/services/http-client.service.ts b/webapp/src/app/services/http-client.service.ts index 88832b7e..f4a3de0a 100644 --- a/webapp/src/app/services/http-client.service.ts +++ b/webapp/src/app/services/http-client.service.ts @@ -30,7 +30,7 @@ export class HttpClientService { } getMaps(): Observable { - const includeVanillaMaps: boolean = this.settingsService.includeVanillaMaps; + const includeVanillaMaps = this.settingsService.getSettings().includeVanillaMaps; return this.request(`api/allMaps?includeVanillaMaps=${includeVanillaMaps}`, api.getAllMaps, includeVanillaMaps); } diff --git a/webapp/src/app/services/phaser/entities/cc-entity.ts b/webapp/src/app/services/phaser/entities/cc-entity.ts index 7e044e08..2cbef319 100644 --- a/webapp/src/app/services/phaser/entities/cc-entity.ts +++ b/webapp/src/app/services/phaser/entities/cc-entity.ts @@ -649,6 +649,7 @@ export abstract class CCEntity extends BaseObject { this.text = this.scene.add.text(0, 0, '', { font: '400 18pt Roboto', color: 'white', + resolution: window.devicePixelRatio * 3 }); this.text.setOrigin(0.5, 0.5); this.text.setScale(0.3); diff --git a/webapp/src/app/services/phaser/tilemap/tile-drawer.ts b/webapp/src/app/services/phaser/tilemap/tile-drawer.ts index 47ccd924..b694b5f0 100644 --- a/webapp/src/app/services/phaser/tilemap/tile-drawer.ts +++ b/webapp/src/app/services/phaser/tilemap/tile-drawer.ts @@ -15,7 +15,7 @@ export class TileDrawer extends BaseObject { private layer?: CCMapLayer; private selectedTiles: SelectedTile[] = []; - private rect?: Phaser.GameObjects.Rectangle; + private selection?: Phaser.GameObjects.Container; private previewTileMap!: Phaser.Tilemaps.Tilemap; private previewLayer?: Phaser.Tilemaps.TilemapLayer; @@ -126,7 +126,7 @@ export class TileDrawer extends BaseObject { diff.y--; } - this.drawRect(diff.x, diff.y, start.x, start.y); + this.drawRect(diff.x, diff.y, start.x, start.y, true); return; } @@ -307,15 +307,64 @@ export class TileDrawer extends BaseObject { this.rightClickStart = p; } - private drawRect(width: number, height: number, x = 0, y = 0) { - if (this.rect) { - this.rect.destroy(); + private drawRect(width: number, height: number, x = 0, y = 0, renderSize = false) { + if (this.selection) { + this.selection.destroy(); } - this.rect = this.scene.add.rectangle(x, y, width * Globals.TILE_SIZE, height * Globals.TILE_SIZE); - this.rect.setOrigin(0, 0); - this.rect.setStrokeStyle(1, 0xffffff, 0.6); - this.container.add(this.rect); + let textColor = 'rgba(0,0,0,0.6)'; + let backgroundColor = 0xffffff; + if (Globals.settingsService.getSettings().selectionBoxDark) { + textColor = 'rgba(255,255,255,0.9)'; + backgroundColor = 0x333333; + } + + this.selection = this.scene.add.container(x, y); + + const rect = this.scene.add.rectangle(0, 0, width * Globals.TILE_SIZE, height * Globals.TILE_SIZE); + rect.setOrigin(0, 0); + rect.setStrokeStyle(1, backgroundColor, 0.6); + + this.selection.add(rect); + this.container.add(this.selection); + + if (!renderSize) { + Globals.globalEventsService.updateTileSelectionSize.next(undefined); + return; + } + + const makeText = (pos: Point, val: number) => { + const text = this.scene.add.text(pos.x, pos.y, Math.abs(val) + '', { + font: '400 10px Roboto', + color: textColor, + resolution: window.devicePixelRatio * 3, + }); + text.setOrigin(0.5, 0); + const background = this.scene.add.rectangle(pos.x, pos.y + 2, 14, 10, backgroundColor, 0.6); + background.setOrigin(0.5, 0); + + this.selection?.add(background); + this.selection?.add(text); + }; + + if (Math.abs(width) >= 3) { + makeText({ + x: width * Globals.TILE_SIZE / 2, + y: (height > 0 ? 0 : height * Globals.TILE_SIZE) - 1 + }, width); + } + + if (Math.abs(height) >= 3) { + makeText({ + x: Globals.TILE_SIZE / 2 + (width > 0 ? 0 : width * Globals.TILE_SIZE), + y: (height - 1) * Globals.TILE_SIZE / 2, + }, height); + } + + Globals.globalEventsService.updateTileSelectionSize.next({ + x: Math.abs(width), + y: Math.abs(height) + }); } private onMouseRightUp() { diff --git a/webapp/src/app/services/settings.service.ts b/webapp/src/app/services/settings.service.ts index 884feaf8..36fa95a7 100644 --- a/webapp/src/app/services/settings.service.ts +++ b/webapp/src/app/services/settings.service.ts @@ -1,28 +1,38 @@ import { Injectable } from '@angular/core'; +export interface AppSettings { + wrapEventEditorLines: boolean; + includeVanillaMaps: boolean; + selectionBoxDark: boolean; +} + @Injectable({ providedIn: 'root' }) export class SettingsService { - private static readonly wrapSettingName = 'wrapEventEditorLines'; - private static readonly includeVanillaMapsSettingName = 'includeVanillaMaps'; - - private static loadBooleanOrDefault(key: string, defaultValue: boolean): boolean { + + private settings: AppSettings = { + wrapEventEditorLines: this.loadBooleanOrDefault('wrapEventEditorLines', true), + includeVanillaMaps: this.loadBooleanOrDefault('includeVanillaMaps', false), + selectionBoxDark: this.loadBooleanOrDefault('selectionBoxDark', true), + }; + + getSettings(): Readonly { + return this.settings; + } + + private loadBooleanOrDefault(key: keyof AppSettings, defaultValue: boolean): boolean { const loadedValue = localStorage.getItem(key); return loadedValue === null ? defaultValue : (loadedValue === 'true'); } - - get wrapEventEditorLines() { - return SettingsService.loadBooleanOrDefault(SettingsService.wrapSettingName, true); - } - set wrapEventEditorLines(value: boolean) { - localStorage.setItem(SettingsService.wrapSettingName, value.toString()); - } - - get includeVanillaMaps() { - return SettingsService.loadBooleanOrDefault(SettingsService.includeVanillaMapsSettingName, false); - } - set includeVanillaMaps(value: boolean) { - localStorage.setItem(SettingsService.includeVanillaMapsSettingName, value.toString()); + + public updateSettings(newSettings: Partial) { + this.settings = { + ...this.settings, + ...newSettings + }; + for (const [key, value] of Object.entries(this.settings)) { + localStorage.setItem(key, (value as boolean).toString()); + } } } diff --git a/webapp/src/assets/selection-dark.png b/webapp/src/assets/selection-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..62e75bcfb4a5e0891972478671b419dea33b3c01 GIT binary patch literal 6820 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 literal 0 HcmV?d00001 diff --git a/webapp/src/assets/selection-light.png b/webapp/src/assets/selection-light.png new file mode 100644 index 0000000000000000000000000000000000000000..b260e360fc3f707838340571bb54911202cc9344 GIT binary patch literal 6788 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 wVrJ}lO;|SC@zuOy@