mirror of https://github.com/Squidex/squidex.git
51 changed files with 171 additions and 2477 deletions
@ -0,0 +1,41 @@ |
|||
// ==========================================================================
|
|||
// WrongEventVersionException.cs
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex Group
|
|||
// All rights reserved.
|
|||
// ==========================================================================
|
|||
|
|||
using System; |
|||
|
|||
namespace Squidex.Infrastructure.CQRS.Events |
|||
{ |
|||
public class WrongEventVersionException : Exception |
|||
{ |
|||
private readonly int currentVersion; |
|||
private readonly int expectedVersion; |
|||
|
|||
public int CurrentVersion |
|||
{ |
|||
get { return currentVersion; } |
|||
} |
|||
|
|||
public int ExpectedVersion |
|||
{ |
|||
get { return expectedVersion; } |
|||
} |
|||
|
|||
public WrongEventVersionException(int currentVersion, int expectedVersion) |
|||
: base(FormatMessage(currentVersion, expectedVersion)) |
|||
{ |
|||
this.currentVersion = currentVersion; |
|||
|
|||
this.expectedVersion = expectedVersion; |
|||
} |
|||
|
|||
private static string FormatMessage(int currentVersion, int expectedVersion) |
|||
{ |
|||
return $"Requested version {expectedVersion}, but found {currentVersion}."; |
|||
} |
|||
} |
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
import { TestBed } from '@angular/core/testing'; |
|||
|
|||
/* tslint:disable ordered-imports */ |
|||
import { RouterModule, provideRoutes } from '@angular/router'; |
|||
import { RouterTestingModule } from '@angular/router/testing'; |
|||
|
|||
import { AppComponent } from './app.component'; |
|||
|
|||
describe('App', () => { |
|||
beforeEach(() => { |
|||
TestBed.configureTestingModule({ |
|||
declarations: [ |
|||
AppComponent |
|||
], |
|||
imports: [ |
|||
RouterModule, |
|||
RouterTestingModule |
|||
], |
|||
providers: [ |
|||
provideRoutes([]) |
|||
] |
|||
}); |
|||
}); |
|||
|
|||
it('should work', () => { |
|||
const fixture = TestBed.createComponent(AppComponent); |
|||
|
|||
expect(fixture.componentInstance instanceof AppComponent).toBe(true, 'should create AppComponent'); |
|||
}); |
|||
}); |
|||
@ -1,65 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Subject } from 'rxjs'; |
|||
|
|||
import { Action } from './../'; |
|||
|
|||
class MockupObject { |
|||
public isDestroyCalled = false; |
|||
|
|||
@Action() |
|||
public event1 = new Subject<string>().map(_ => { return { type: 'MOCK_ACTION' }; }); |
|||
|
|||
@Action() |
|||
public event2 = new Subject<string>().map(_ => { return { type: 'MOCK_ACTION' }; }); |
|||
|
|||
constructor(private readonly store: any) { } |
|||
|
|||
public log() { |
|||
this.store.log(); |
|||
} |
|||
|
|||
public init() { |
|||
this.event2 = new Subject<string>().map(_ => { return { type: 'MOCK_ACTION' }; }); |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
this.isDestroyCalled = true; |
|||
} |
|||
} |
|||
|
|||
describe('Action', () => { |
|||
it('should make complete flow to subscribe and unsubscribe and to trigger actions', () => { |
|||
let dispatchCount = 0; |
|||
|
|||
const state = { |
|||
next: (e: any) => { |
|||
dispatchCount++; |
|||
|
|||
expect(e.type).toBe('MOCK_ACTION'); |
|||
} |
|||
}; |
|||
|
|||
const mock = new MockupObject(state); |
|||
|
|||
mock.init(); |
|||
|
|||
(<any>mock.event1).next('TEST'); |
|||
(<any>mock.event2).next('TEST'); |
|||
|
|||
expect(dispatchCount).toBe(2); |
|||
|
|||
mock.ngOnDestroy(); |
|||
|
|||
(<any>mock.event1).next('TEST'); |
|||
(<any>mock.event2).next('TEST'); |
|||
|
|||
expect(dispatchCount).toBe(2); |
|||
expect(mock.isDestroyCalled).toBeTruthy(); |
|||
}); |
|||
}); |
|||
@ -1,72 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
/* tslint:disable:no-empty */ |
|||
/* tslint:disable:only-arrow-functions */ |
|||
|
|||
const EMPTY_FUNC = () => {}; |
|||
|
|||
export function Action() { |
|||
return function (target: any, key: string) { |
|||
let observable: any; |
|||
let instance: any; |
|||
let subscriptions: any; |
|||
let subscription: any; |
|||
|
|||
function subscribe() { |
|||
const store = instance.store; |
|||
|
|||
if (store && observable && observable.subscribe && typeof observable.subscribe === 'function') { |
|||
subscription = observable.subscribe((a: any) => { if (a) { store.next(a); } }); |
|||
|
|||
subscriptions.push(subscription); |
|||
} |
|||
}; |
|||
|
|||
function unsubscribe() { |
|||
if (subscription) { |
|||
subscription.unsubscribe(); |
|||
|
|||
subscriptions.splice(subscriptions.indexOf(subscribe), 1); |
|||
} |
|||
}; |
|||
|
|||
if (delete target[key]) { |
|||
Object.defineProperty(target, key, { |
|||
get: function () { |
|||
return observable; |
|||
}, |
|||
set: function (v) { |
|||
instance = this; |
|||
|
|||
if (!instance.___subscriptions) { |
|||
instance.___subscriptions = []; |
|||
|
|||
let destroy = instance.ngOnDestroy ? instance.ngOnDestroy.bind(instance) : EMPTY_FUNC; |
|||
|
|||
instance.ngOnDestroy = () => { |
|||
for (let s of subscriptions) { |
|||
s.unsubscribe(); |
|||
} |
|||
|
|||
instance.___subscriptions = null; |
|||
|
|||
destroy(); |
|||
}; |
|||
} |
|||
|
|||
subscriptions = instance.___subscriptions; |
|||
|
|||
observable = v; |
|||
|
|||
unsubscribe(); |
|||
subscribe(); |
|||
} |
|||
}); |
|||
} |
|||
}; |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
<div class="btn-group" [class.open]="isOpen"> |
|||
<button type="button" class="btn btn-secondary btn-sm" [disabled]="isDisabled" (click)="toggleOpen()" [style.background]="selectedColor.toString()"></button> |
|||
|
|||
<div class="dropdown-menu dropdown-menu-{{dropdownSide}}"> |
|||
<div class="color-palette"> |
|||
<span *ngFor="let color of palette.colors" class="color-palette-box" (click)="selectColor(color)" [class.selected]="selectedColor && color.eq(selectedColor)"> |
|||
<span class="color-palette-color" [style.background]="color.toString()"></span> |
|||
</span> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
@ -1,68 +0,0 @@ |
|||
@import '_mixins'; |
|||
@import '_vars'; |
|||
|
|||
$color-size: 24px; |
|||
$color-button: #1875cc; |
|||
$color-button-hover: #1460a8; |
|||
$color-border: #fff; |
|||
$button-size: 1.81rem; |
|||
|
|||
.color-palette { |
|||
& { |
|||
@include clearfix; |
|||
padding: 6px; |
|||
width: 8 * ($color-size + 6); |
|||
} |
|||
|
|||
&-box { |
|||
& { |
|||
border: 2px solid transparent; |
|||
margin: 2px; |
|||
height: $color-size; |
|||
width: $color-size; |
|||
float: left; |
|||
} |
|||
|
|||
&:hover { |
|||
border-color: $color-button-hover; |
|||
} |
|||
|
|||
&.selected { |
|||
& { |
|||
border-color: $color-button; |
|||
} |
|||
|
|||
&:hover { |
|||
border-color: $color-button; |
|||
} |
|||
} |
|||
|
|||
&.disabled { |
|||
border-color: transparent; |
|||
} |
|||
} |
|||
|
|||
&-color { |
|||
border: 1px solid $color-border; |
|||
width: $color-size - 4; |
|||
height: $color-size - 4; |
|||
display: block; |
|||
} |
|||
} |
|||
|
|||
.dropdown-menu { |
|||
background: $color-border; |
|||
} |
|||
|
|||
.btn-group { |
|||
> .btn { |
|||
&:first-child { |
|||
@include border-radius(.25em); |
|||
} |
|||
} |
|||
} |
|||
|
|||
.btn { |
|||
width: $button-size; |
|||
height: $button-size; |
|||
} |
|||
@ -1,117 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Color } from './../'; |
|||
import { ColorPickerComponent } from './color-picker.component'; |
|||
|
|||
describe('ColorPickerComponent', () => { |
|||
it('should instantiate', () => { |
|||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|||
|
|||
expect(colorPicker).toBeDefined(); |
|||
}); |
|||
|
|||
it('should close color picker when clicking outside of the modal', () => { |
|||
const element = { |
|||
nativeElement: { |
|||
contains: () => { |
|||
return false; |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const colorPicker = new ColorPickerComponent(element); |
|||
colorPicker.open(); |
|||
colorPicker.onClick({}); |
|||
|
|||
expect(colorPicker.isOpen).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should not close color picker when clicking inside the modal', () => { |
|||
const element = { |
|||
nativeElement: { |
|||
contains: () => { |
|||
return true; |
|||
} |
|||
} |
|||
}; |
|||
|
|||
const colorPicker = new ColorPickerComponent(element); |
|||
colorPicker.open(); |
|||
colorPicker.onClick({}); |
|||
|
|||
expect(colorPicker.isOpen).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should close modal and emit event when setting color', () => { |
|||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|||
const selectedColor = Color.RED; |
|||
|
|||
let lastColor: Color | null = null; |
|||
|
|||
colorPicker.registerOnChange((c: Color) => { |
|||
lastColor = c; |
|||
}); |
|||
|
|||
colorPicker.open(); |
|||
colorPicker.selectColor(selectedColor); |
|||
|
|||
expect(lastColor).toBe(selectedColor); |
|||
expect(colorPicker.isOpen).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should not emit event when selecting same color', () => { |
|||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|||
const selectedColor = Color.RED; |
|||
|
|||
colorPicker.selectColor(selectedColor); |
|||
|
|||
let lastColor: Color | null = null; |
|||
|
|||
colorPicker.registerOnChange((c: Color) => { |
|||
lastColor = c; |
|||
}); |
|||
|
|||
colorPicker.selectColor(selectedColor); |
|||
|
|||
expect(lastColor).toBeNull(); |
|||
}); |
|||
|
|||
it('should update selected color when writing value', () => { |
|||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|||
const selectedColor = Color.RED; |
|||
|
|||
colorPicker.writeValue(selectedColor); |
|||
|
|||
expect(colorPicker.selectedColor).toBe(selectedColor); |
|||
}); |
|||
|
|||
it('should update selected color with palette default if setting invalid color', () => { |
|||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|||
|
|||
colorPicker.writeValue('invalid'); |
|||
|
|||
expect(colorPicker.selectedColor).toBe(colorPicker.palette.defaultColor); |
|||
}); |
|||
|
|||
it('should update selected color with black if setting invalid color and palette is null', () => { |
|||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|||
|
|||
colorPicker.palette = undefined!; |
|||
colorPicker.writeValue('invalid'); |
|||
|
|||
expect(colorPicker.selectedColor).toBe(Color.BLACK); |
|||
}); |
|||
|
|||
it('should update isOpen prperty when toggleOpen is invoked', () => { |
|||
const colorPicker = new ColorPickerComponent({ nativeElement: {} }); |
|||
|
|||
colorPicker.toggleOpen(); |
|||
|
|||
expect(colorPicker.isOpen).toBeTruthy(); |
|||
}); |
|||
}); |
|||
@ -1,125 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Component, ElementRef, forwardRef, HostListener, Input } from '@angular/core'; |
|||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; |
|||
|
|||
import { Color } from './../utils/color'; |
|||
import { ColorPalette } from './../utils/color-palette'; |
|||
|
|||
/* tslint:disable:no-empty */ |
|||
|
|||
const NOOP = () => { }; |
|||
|
|||
export const SQX_COLOR_PICKER_CONTROL_VALUE_ACCESSOR: any = { |
|||
provide: NG_VALUE_ACCESSOR, |
|||
useExisting: forwardRef(() => ColorPickerComponent), |
|||
multi: true |
|||
}; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-color-picker', |
|||
styleUrls: ['./color-picker.component.scss'], |
|||
templateUrl: './color-picker.component.html', |
|||
providers: [SQX_COLOR_PICKER_CONTROL_VALUE_ACCESSOR] |
|||
}) |
|||
export class ColorPickerComponent implements ControlValueAccessor { |
|||
private changeCallback: (value: any) => void = NOOP; |
|||
private touchedCallback: () => void = NOOP; |
|||
|
|||
public selectedColor: Color = Color.BLACK; |
|||
|
|||
@Input() |
|||
public palette = ColorPalette.colors(); |
|||
|
|||
@Input() |
|||
public dropdownSide: 'left'; |
|||
|
|||
@Input() |
|||
public isOpen = false; |
|||
|
|||
@Input() |
|||
public isDisabled = false; |
|||
|
|||
constructor(private readonly element: ElementRef) { |
|||
this.updateColor(Color.BLACK); |
|||
} |
|||
|
|||
public writeValue(value: any) { |
|||
this.updateColor(value); |
|||
} |
|||
|
|||
public setDisabledState(isDisabled: boolean): void { |
|||
this.isDisabled = isDisabled; |
|||
} |
|||
|
|||
public registerOnChange(fn: any) { |
|||
this.changeCallback = fn; |
|||
} |
|||
|
|||
public registerOnTouched(fn: any) { |
|||
this.touchedCallback = fn; |
|||
} |
|||
|
|||
@HostListener('document:click', ['$event.target']) |
|||
public onClick(targetElement: any) { |
|||
const clickedInside = this.element.nativeElement.contains(targetElement); |
|||
|
|||
if (!clickedInside) { |
|||
this.close(); |
|||
} |
|||
} |
|||
|
|||
public toggleOpen() { |
|||
if (this.isOpen) { |
|||
this.close(); |
|||
} else { |
|||
this.open(); |
|||
} |
|||
} |
|||
|
|||
public open() { |
|||
this.isOpen = true; |
|||
} |
|||
|
|||
public close() { |
|||
this.isOpen = false; |
|||
|
|||
this.touchedCallback(); |
|||
} |
|||
|
|||
public selectColor(color: Color) { |
|||
this.updateParent(color); |
|||
this.updateColor(color); |
|||
this.close(); |
|||
} |
|||
|
|||
private updateParent(color: Color) { |
|||
if (this.selectedColor.ne(color)) { |
|||
this.changeCallback(color); |
|||
} |
|||
} |
|||
|
|||
private updateColor(color: Color) { |
|||
let hasColor = false; |
|||
try { |
|||
this.selectedColor = Color.fromValue(color); |
|||
|
|||
hasColor = true; |
|||
} catch (e) { |
|||
hasColor = false; |
|||
} |
|||
|
|||
if (!hasColor || !this.selectedColor) { |
|||
if (this.palette) { |
|||
this.selectedColor = this.palette.defaultColor; |
|||
} else { |
|||
this.selectedColor = Color.BLACK; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,118 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Directive, ElementRef, HostListener, Input, Renderer }from '@angular/core'; |
|||
|
|||
import { DragService } from './../services/drag.service'; |
|||
import { Vec2 } from './../utils/vec2'; |
|||
|
|||
@Directive({ |
|||
selector: '[sqxDragModel]' |
|||
}) |
|||
export class DragModelDirective { |
|||
private startOffset: Vec2; |
|||
private startPosition: Vec2; |
|||
private mouseMoveSubscription: Function | null; |
|||
private mouseUpSubscription: Function | null; |
|||
private clonedElement: HTMLElement | null; |
|||
|
|||
@Input('sqxDragModel') |
|||
public model: any; |
|||
|
|||
constructor( |
|||
private readonly element: ElementRef, |
|||
private readonly renderer: Renderer, |
|||
private readonly dragService: DragService |
|||
) { |
|||
} |
|||
|
|||
@HostListener('mousedown', ['$event']) |
|||
public onMouseDown(event: MouseEvent) { |
|||
this.startOffset = new Vec2(event.offsetX, event.offsetY); |
|||
this.startPosition = new Vec2(event.clientX, event.clientY); |
|||
|
|||
this.mouseMoveSubscription = |
|||
this.renderer.listenGlobal('window', 'mousemove', (e: MouseEvent) => { |
|||
this.onMouseMove(e); |
|||
}); |
|||
|
|||
this.mouseUpSubscription = |
|||
this.renderer.listenGlobal('window', 'mouseup', (e: MouseEvent) => { |
|||
this.onMouseUp(e); |
|||
}); |
|||
|
|||
this.stopEvent(event); |
|||
} |
|||
|
|||
private onMouseMove(event: MouseEvent) { |
|||
const position = new Vec2(event.clientX, event.clientY); |
|||
|
|||
if (!this.clonedElement && position.sub(this.startPosition).lengtSquared > 100) { |
|||
this.clonedElement = this.element.nativeElement.cloneNode(true); |
|||
|
|||
this.clonedElement!.style.position = 'fixed'; |
|||
this.clonedElement!.style.zIndex = '10000'; |
|||
|
|||
document.body.appendChild(this.clonedElement!); |
|||
} |
|||
|
|||
if (this.clonedElement) { |
|||
const elementPosition = position.sub(this.startOffset); |
|||
|
|||
this.clonedElement.style.left = elementPosition.x + 'px'; |
|||
this.clonedElement.style.top = elementPosition.y + 'px'; |
|||
} |
|||
|
|||
this.stopEvent(event); |
|||
} |
|||
|
|||
private onMouseUp(event: MouseEvent) { |
|||
if (this.clonedElement) { |
|||
this.clonedElement.remove(); |
|||
} |
|||
|
|||
let dropCandidate: Element | null = document.elementFromPoint(event.clientX, event.clientY); |
|||
|
|||
while (dropCandidate && (dropCandidate.classList && !dropCandidate.classList.contains('sqx-drop'))) { |
|||
dropCandidate = dropCandidate.parentNode as Element; |
|||
} |
|||
|
|||
if (dropCandidate && dropCandidate.id) { |
|||
const position = this.getRelativeCoordinates(event, dropCandidate).sub(this.startOffset).round(); |
|||
|
|||
this.dragService.emitDrop({ position, model: this.model, dropTarget: dropCandidate.id }); |
|||
} |
|||
|
|||
if (this.mouseMoveSubscription) { |
|||
this.mouseMoveSubscription(); |
|||
this.mouseMoveSubscription = null; |
|||
} |
|||
|
|||
if (this.mouseUpSubscription) { |
|||
this.mouseUpSubscription(); |
|||
this.mouseUpSubscription = null; |
|||
} |
|||
|
|||
this.clonedElement = null; |
|||
|
|||
this.stopEvent(event); |
|||
} |
|||
|
|||
private stopEvent(event: Event) { |
|||
event.preventDefault(); |
|||
event.stopPropagation(); |
|||
} |
|||
|
|||
private getRelativeCoordinates(e: any, container: any): Vec2 { |
|||
const rect = container.getBoundingClientRect(); |
|||
|
|||
const x = !!e.touches ? e.touches[0].pageX : e.pageX; |
|||
const y = !!e.touches ? e.touches[0].pageY : e.pageY; |
|||
|
|||
return new Vec2(x - rect.left, y - rect.top); |
|||
} |
|||
} |
|||
@ -1,128 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Directive, ElementRef, HostListener } from '@angular/core'; |
|||
|
|||
import { DragService } from './../services/drag.service'; |
|||
import { Vec2 } from './../utils/vec2'; |
|||
|
|||
@Directive({ |
|||
selector: '.sqx-image-drop' |
|||
}) |
|||
export class ImageDropDirective { |
|||
constructor( |
|||
private readonly element: ElementRef, |
|||
private readonly dragService: DragService |
|||
) { |
|||
} |
|||
|
|||
@HostListener('dragenter', ['$event']) |
|||
public onDragEnter(event: DragDropEvent) { |
|||
this.tryStopEvent(event); |
|||
} |
|||
|
|||
@HostListener('dragover', ['$event']) |
|||
public onDragOver(event: DragDropEvent) { |
|||
this.tryStopEvent(event); |
|||
} |
|||
|
|||
@HostListener('drop', ['$event']) |
|||
public onDrop(event: DragDropEvent) { |
|||
const image = this.findImage(event); |
|||
|
|||
if (!image) { |
|||
return; |
|||
} |
|||
|
|||
const position = this.getRelativeCoordinates(event, this.element.nativeElement).round(); |
|||
|
|||
const reader = new FileReader(); |
|||
|
|||
reader.onload = (loadedFile: any) => { |
|||
const imageSource: string = loadedFile.target.result; |
|||
const imageElement = document.createElement('img'); |
|||
|
|||
imageElement.onload = () => { |
|||
this.dragService.emitDrop({ |
|||
position, dropTarget: this.element.nativeElement.id, model: { |
|||
sizeX: imageElement.width, |
|||
sizeY: imageElement.height, |
|||
source: imageSource |
|||
} |
|||
}); |
|||
}; |
|||
imageElement.src = imageSource; |
|||
}; |
|||
reader.readAsDataURL(image); |
|||
|
|||
this.stopEvent(event); |
|||
} |
|||
|
|||
private stopEvent(event: Event) { |
|||
event.preventDefault(); |
|||
event.stopPropagation(); |
|||
} |
|||
|
|||
private tryStopEvent(event: DragDropEvent) { |
|||
const hasFiles = this.hasFiles(event.dataTransfer.types); |
|||
|
|||
if (!hasFiles) { |
|||
return; |
|||
} |
|||
|
|||
this.stopEvent(event); |
|||
} |
|||
|
|||
private hasFiles(types: any): boolean { |
|||
if (!types) { |
|||
return false; |
|||
} |
|||
|
|||
if (isFunction(types.indexOf)) { |
|||
return types.indexOf('Files') !== -1; |
|||
} else if (isFunction(types.contains)) { |
|||
return types.contains('Files'); |
|||
} else { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
private findImage(event: DragDropEvent): File | null { |
|||
let image: File | null = null; |
|||
|
|||
/* tslint:disable:prefer-for-of */ |
|||
for (let i = 0; i < event.dataTransfer.files.length; i++) { |
|||
const file = event.dataTransfer.files[i]; |
|||
|
|||
if (file.type.match('image.*')) { |
|||
image = file; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return image; |
|||
} |
|||
|
|||
private getRelativeCoordinates(e: any, container: any): Vec2 { |
|||
const rect = container.getBoundingClientRect(); |
|||
|
|||
const pos = { x: 0, y: 0 }; |
|||
|
|||
pos.x = !!e.touches ? e.touches[0].pageX : e.pageX; |
|||
pos.y = !!e.touches ? e.touches[0].pageY : e.pageY; |
|||
|
|||
return new Vec2(pos.x - rect.left, pos.y - rect.top); |
|||
} |
|||
} |
|||
|
|||
function isFunction(obj: any): boolean { |
|||
return !!(obj && obj.constructor && obj.call && obj.apply); |
|||
}; |
|||
|
|||
export interface DragDropEvent extends MouseEvent { |
|||
readonly dataTransfer: DataTransfer; |
|||
} |
|||
@ -1,40 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Component, ElementRef } from '@angular/core'; |
|||
|
|||
declare var Spinner: any; |
|||
|
|||
@Component({ |
|||
selector: 'sqx-spinner', |
|||
template: '' |
|||
}) |
|||
export class SpinnerComponent { |
|||
constructor(element: ElementRef) { |
|||
const mediumOptions = { |
|||
lines: 12, |
|||
length: 5, |
|||
width: 2, |
|||
radius: 6, |
|||
corners: 1, |
|||
rotate: 0, |
|||
direction: 1, |
|||
color: '#000', |
|||
speed: 1.5, |
|||
trail: 40, |
|||
shadow: false, |
|||
hwaccel: false, |
|||
className: 'spinner', |
|||
zIndex: 0, |
|||
position: 'relative' |
|||
}; |
|||
|
|||
element.nativeElement.classList.add('spinner-medium'); |
|||
|
|||
new Spinner(mediumOptions).spin(element.nativeElement); |
|||
} |
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { |
|||
DragService, |
|||
DragServiceFactory, |
|||
DropEvent |
|||
} from './../'; |
|||
|
|||
describe('DragService', () => { |
|||
it('should instantiate from factory', () => { |
|||
const dragService = DragServiceFactory(); |
|||
|
|||
expect(dragService).toBeDefined(); |
|||
}); |
|||
|
|||
it('should instantiate', () => { |
|||
const dragService = new DragService(); |
|||
|
|||
expect(dragService).toBeDefined(); |
|||
}); |
|||
|
|||
it('should raise event handler when dropped', () => { |
|||
let emittedEvent: DropEvent | null = null; |
|||
|
|||
const dragService = new DragService(); |
|||
|
|||
dragService.onDrop.subscribe(e => { |
|||
emittedEvent = e; |
|||
}); |
|||
|
|||
const event: DropEvent = { position: null!, model: null!, dropTarget: null! }; |
|||
|
|||
dragService.emitDrop(event); |
|||
|
|||
expect(emittedEvent).toBe(event); |
|||
}); |
|||
}); |
|||
@ -1,30 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Injectable } from '@angular/core'; |
|||
import { Observable, Subject } from 'rxjs'; |
|||
|
|||
import { Vec2 } from './../utils/vec2'; |
|||
|
|||
export interface DropEvent { position: Vec2; model: any; dropTarget: string; } |
|||
|
|||
export const DragServiceFactory = () => { |
|||
return new DragService(); |
|||
}; |
|||
|
|||
@Injectable() |
|||
export class DragService { |
|||
private readonly dropEvent = new Subject<DropEvent>(); |
|||
|
|||
public get onDrop(): Observable<DropEvent> { |
|||
return this.dropEvent; |
|||
} |
|||
|
|||
public emitDrop(event: DropEvent) { |
|||
this.dropEvent.next(event); |
|||
} |
|||
} |
|||
@ -1,74 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { ArrayHelper } from './../'; |
|||
|
|||
describe('ArrayHelper', () => { |
|||
it('should push item', () => { |
|||
const oldArray = [1, 2, 3]; |
|||
const newArray = ArrayHelper.push(oldArray, 4); |
|||
|
|||
expect(newArray).toEqual([1, 2, 3, 4]); |
|||
}); |
|||
|
|||
it('should remove item if found', () => { |
|||
const oldArray = [1, 2, 3]; |
|||
const newArray = ArrayHelper.remove(oldArray, 2); |
|||
|
|||
expect(newArray).toEqual([1, 3]); |
|||
}); |
|||
|
|||
it('should not remove item if not found', () => { |
|||
const oldArray = [1, 2, 3]; |
|||
const newArray = ArrayHelper.remove(oldArray, 5); |
|||
|
|||
expect(newArray).toEqual([1, 2, 3]); |
|||
}); |
|||
|
|||
it('should remove all by predicate', () => { |
|||
const oldArray: number[] = [1, 2, 3, 4]; |
|||
const newArray = ArrayHelper.removeAll(oldArray, (i: number) => i % 2 === 0); |
|||
|
|||
expect(newArray).toEqual([1, 3]); |
|||
}); |
|||
|
|||
it('should return original if nothing has been removed', () => { |
|||
const oldArray: number[] = [1, 2, 3, 4]; |
|||
const newArray = ArrayHelper.removeAll(oldArray, (i: number) => i % 200 === 0); |
|||
|
|||
expect(newArray).toEqual(oldArray); |
|||
}); |
|||
|
|||
it('should replace item if found', () => { |
|||
const oldArray = [1, 2, 3]; |
|||
const newArray = ArrayHelper.replace(oldArray, 2, 4); |
|||
|
|||
expect(newArray).toEqual([1, 4, 3]); |
|||
}); |
|||
|
|||
it('should not replace item if not found', () => { |
|||
const oldArray = [1, 2, 3]; |
|||
const newArray = ArrayHelper.replace(oldArray, 5, 5); |
|||
|
|||
expect(newArray).toEqual([1, 2, 3]); |
|||
}); |
|||
|
|||
it('should replace all by predicate', () => { |
|||
const oldArray: number[] = [1, 2, 3, 4]; |
|||
const newArray = ArrayHelper.replaceAll(oldArray, (i: number) => i % 2 === 0, i => i * 2); |
|||
|
|||
expect(newArray).toEqual([1, 4, 3, 8]); |
|||
}); |
|||
|
|||
it('should return original if nothing has been replace', () => { |
|||
const oldArray: number[] = [1, 2, 3, 4]; |
|||
const newArray = ArrayHelper.replaceAll(oldArray, (i: number) => i % 200 === 0, i => i); |
|||
|
|||
expect(newArray).toEqual(oldArray); |
|||
}); |
|||
|
|||
}); |
|||
@ -1,74 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
export module ArrayHelper { |
|||
export function push<T>(array: T[], item: T): T[] { |
|||
return [...array, item]; |
|||
} |
|||
|
|||
export function remove<T>(array: T[], item: T): T[] { |
|||
const index = array.indexOf(item); |
|||
|
|||
if (index >= 0) { |
|||
return [...array.slice(0, index), ...array.slice(index + 1)]; |
|||
} else { |
|||
return array; |
|||
} |
|||
} |
|||
|
|||
export function removeAll<T>(array: T[], predicate: (item: T, index: number) => boolean): T[] { |
|||
const copy = array.slice(); |
|||
|
|||
let hasChange = false; |
|||
|
|||
for (let i = 0; i < copy.length; ) { |
|||
if (predicate(copy[i], i)) { |
|||
copy.splice(i, 1); |
|||
|
|||
hasChange = true; |
|||
} else { |
|||
++i; |
|||
} |
|||
} |
|||
|
|||
return hasChange ? copy : array; |
|||
} |
|||
|
|||
export function replace<T>(array: T[], oldItem: T, newItem: T): T[] { |
|||
const index = array.indexOf(oldItem); |
|||
|
|||
if (index >= 0) { |
|||
const copy = array.slice(); |
|||
|
|||
copy[index] = newItem; |
|||
|
|||
return copy; |
|||
} else { |
|||
return array; |
|||
} |
|||
} |
|||
|
|||
export function replaceAll<T>(array: T[], predicate: (item: T, index: number) => boolean, replacer: (item: T) => T): T[] { |
|||
const copy = array.slice(); |
|||
|
|||
let hasChange = false; |
|||
|
|||
for (let i = 0; i < copy.length; i++) { |
|||
if (predicate(copy[i], i)) { |
|||
const newItem = replacer(copy[i]); |
|||
|
|||
if (copy[i] !== newItem) { |
|||
copy[i] = newItem; |
|||
|
|||
hasChange = true; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return hasChange ? copy : array; |
|||
} |
|||
} |
|||
@ -1,16 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { ColorPalette } from './../'; |
|||
|
|||
describe('ColorPalatte', () => { |
|||
it('should generate colors', () => { |
|||
const palette = ColorPalette.colors(); |
|||
|
|||
expect(palette.colors.length).toBeGreaterThan(20); |
|||
}); |
|||
}); |
|||
@ -1,41 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Color } from './color'; |
|||
|
|||
const BREWERS = { |
|||
greys: [0xffffff, 0xF0F0F0, 0xbdbdbd, 0x969696, 0x737373, 0x525252, 0x252525, 0x000000], |
|||
oranges: [0xfee6ce, 0xfdd0a2, 0xfdae6b, 0xfd8d3c, 0xf16913, 0xd94801, 0xa63603, 0x7f2704], |
|||
reds: [0xfee0d2, 0xfcbba1, 0xfc9272, 0xfb6a4a, 0xef3b2c, 0xcb181d, 0xa50f15, 0x67000d], |
|||
greens: [0xe5f5e0, 0xc7e9c0, 0xa1d99b, 0x74c476, 0x41ab5d, 0x238b45, 0x006d2c, 0x00441b], |
|||
purples: [0xefedf5, 0xdadaeb, 0xbcbddc, 0x9e9ac8, 0x807dba, 0x6a51a3, 0x54278f, 0x3f007d], |
|||
blues: [0xdeebf7, 0xc6dbef, 0x9ecae1, 0x6baed6, 0x4292c6, 0x2171b5, 0x08519c, 0x08306b] |
|||
}; |
|||
|
|||
export class ColorPalette { |
|||
constructor( |
|||
public readonly colors: Color[], |
|||
public readonly defaultColor: Color |
|||
) { |
|||
} |
|||
|
|||
public static colors(): ColorPalette { |
|||
const colors: Color[] = []; |
|||
|
|||
for (let key in BREWERS) { |
|||
if (BREWERS.hasOwnProperty(key)) { |
|||
const brewer = BREWERS[key]; |
|||
|
|||
for (let color of brewer) { |
|||
colors.push(Color.fromNumber(color)); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new ColorPalette(colors, colors[7]); |
|||
} |
|||
} |
|||
@ -1,273 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Color } from './../'; |
|||
|
|||
describe('Color', () => { |
|||
it('should instantiate', () => { |
|||
const color = new Color(0.3, 0.6, 0.9); |
|||
|
|||
expect(color.r).toBe(0.3); |
|||
expect(color.g).toBe(0.6); |
|||
expect(color.b).toBe(0.9); |
|||
}); |
|||
|
|||
it('should adjust values when instantiating', () => { |
|||
const color = new Color(-1, 0.5, 2); |
|||
|
|||
expect(color.r).toBe(0); |
|||
expect(color.g).toBe(0.5); |
|||
expect(color.b).toBe(1); |
|||
}); |
|||
|
|||
it('should convert to color', () => { |
|||
const color = Color.fromHex(23, 46, 59); |
|||
|
|||
expect(color.toNumber()).toBe(0x234659); |
|||
}); |
|||
|
|||
it('should convert to string', () => { |
|||
const color = Color.fromHex(23, 46, 59); |
|||
|
|||
expect(color.toString()).toBe('#234659'); |
|||
}); |
|||
|
|||
it('should convert to string with leading zeros', () => { |
|||
const color = Color.fromHex(3, 6, 9); |
|||
|
|||
expect(color.toString()).toBe('#030609'); |
|||
}); |
|||
|
|||
it('should be created from long string', () => { |
|||
const color = Color.fromValue('#336699'); |
|||
|
|||
expect(color.r).toBe(0.2); |
|||
expect(color.g).toBe(0.4); |
|||
expect(color.b).toBe(0.6); |
|||
}); |
|||
|
|||
it('should be created from short string', () => { |
|||
const color = Color.fromValue('#369'); |
|||
|
|||
expect(color.r).toBe(0.2); |
|||
expect(color.g).toBe(0.4); |
|||
expect(color.b).toBe(0.6); |
|||
}); |
|||
|
|||
it('should be created from rgb string', () => { |
|||
const color = Color.fromValue('rgb(51, 102, 153)'); |
|||
|
|||
expect(color.r).toBe(0.2); |
|||
expect(color.g).toBe(0.4); |
|||
expect(color.b).toBe(0.6); |
|||
}); |
|||
|
|||
it('should be created from rgb number', () => { |
|||
const color = Color.fromValue(0x336699); |
|||
|
|||
expect(color.r).toBe(0.2); |
|||
expect(color.g).toBe(0.4); |
|||
expect(color.b).toBe(0.6); |
|||
}); |
|||
|
|||
it('should be created from rgb values', () => { |
|||
const color = Color.fromHex(33, 66, 99); |
|||
|
|||
expect(color.r).toBe(0.2); |
|||
expect(color.g).toBe(0.4); |
|||
expect(color.b).toBe(0.6); |
|||
}); |
|||
|
|||
it('should convert from hsl with black', () => { |
|||
const color = Color.fromHsl(0, 0, 0); |
|||
|
|||
expect(color.r).toBe(0); |
|||
expect(color.g).toBe(0); |
|||
expect(color.b).toBe(0); |
|||
}); |
|||
|
|||
it('should convert from hsl with dark black', () => { |
|||
const color = Color.fromHsl(0, 1, 0.1); |
|||
|
|||
expect(color.r).toBe(0.2); |
|||
expect(color.g).toBe(0); |
|||
expect(color.b).toBe(0); |
|||
}); |
|||
|
|||
it('should convert from hsl with red', () => { |
|||
const color = Color.fromHsl(0, 1, 0.5); |
|||
|
|||
expect(color.r).toBeCloseTo(1, 1); |
|||
expect(color.g).toBe(0); |
|||
expect(color.b).toBe(0); |
|||
}); |
|||
|
|||
it('should convert from hsl with yellow', () => { |
|||
const color = Color.fromHsl(60, 1, 0.5); |
|||
|
|||
expect(color.r).toBeCloseTo(1, 1); |
|||
expect(color.g).toBeCloseTo(1, 1); |
|||
expect(color.b).toBe(0); |
|||
}); |
|||
|
|||
it('should convert from hsl with blue', () => { |
|||
const color = Color.fromHsl(120, 1, 0.5); |
|||
|
|||
expect(color.r).toBe(0); |
|||
expect(color.g).toBe(1); |
|||
expect(color.b).toBe(0); |
|||
}); |
|||
|
|||
it('should convert from hsl with turkis', () => { |
|||
const color = Color.fromHsl(180, 1, 0.5); |
|||
|
|||
expect(color.r).toBe(0); |
|||
expect(color.g).toBeCloseTo(1, 1); |
|||
expect(color.b).toBeCloseTo(1, 1); |
|||
}); |
|||
|
|||
it('should convert from hsv with red', () => { |
|||
const color = Color.fromHsv(240, 1, 1); |
|||
|
|||
expect(color.r).toBe(0); |
|||
expect(color.g).toBe(0); |
|||
expect(color.b).toBe(1); |
|||
}); |
|||
|
|||
it('should convert from hsv with pink', () => { |
|||
const color = Color.fromHsv(300, 1, 1); |
|||
|
|||
expect(color.r).toBe(1); |
|||
expect(color.g).toBe(0); |
|||
expect(color.b).toBe(1); |
|||
}); |
|||
|
|||
it('should convert from hsl with another red', () => { |
|||
const color = Color.fromHsl(360, 1, 0.5); |
|||
|
|||
expect(color.r).toBeCloseTo(1, 1); |
|||
expect(color.g).toBe(0); |
|||
expect(color.b).toBe(0); |
|||
}); |
|||
|
|||
it('should convert from hsv with red', () => { |
|||
const color = Color.fromHsv(0, 1, 1); |
|||
|
|||
expect(color.r).toBe(1); |
|||
expect(color.g).toBe(0); |
|||
expect(color.b).toBe(0); |
|||
}); |
|||
|
|||
it('should convert from hsv with yellow', () => { |
|||
const color = Color.fromHsv(60, 1, 1); |
|||
|
|||
expect(color.r).toBe(1); |
|||
expect(color.g).toBe(1); |
|||
expect(color.b).toBe(0); |
|||
}); |
|||
|
|||
it('should convert from hsv with blue', () => { |
|||
const color = Color.fromHsv(120, 1, 1); |
|||
|
|||
expect(color.r).toBe(0); |
|||
expect(color.g).toBe(1); |
|||
expect(color.b).toBe(0); |
|||
}); |
|||
|
|||
it('should convert from hsv with turkis', () => { |
|||
const color = Color.fromHsv(180, 1, 1); |
|||
|
|||
expect(color.r).toBe(0); |
|||
expect(color.g).toBe(1); |
|||
expect(color.b).toBe(1); |
|||
}); |
|||
|
|||
it('should convert from hsv with red', () => { |
|||
const color = Color.fromHsv(240, 1, 1); |
|||
|
|||
expect(color.r).toBe(0); |
|||
expect(color.g).toBe(0); |
|||
expect(color.b).toBe(1); |
|||
}); |
|||
|
|||
it('should convert from hsv with pink', () => { |
|||
const color = Color.fromHsv(300, 1, 1); |
|||
|
|||
expect(color.r).toBe(1); |
|||
expect(color.g).toBe(0); |
|||
expect(color.b).toBe(1); |
|||
}); |
|||
|
|||
it('should be valid black', () => { |
|||
const color = Color.BLACK; |
|||
|
|||
expect(color.r).toBe(0); |
|||
expect(color.g).toBe(0); |
|||
expect(color.b).toBe(0); |
|||
}); |
|||
|
|||
it('should be valid white', () => { |
|||
const color = Color.WHITE; |
|||
|
|||
expect(color.r).toBe(1); |
|||
expect(color.g).toBe(1); |
|||
expect(color.b).toBe(1); |
|||
}); |
|||
|
|||
it('should be valid red', () => { |
|||
const color = Color.RED; |
|||
|
|||
expect(color.r).toBe(1); |
|||
expect(color.g).toBe(0); |
|||
expect(color.b).toBe(0); |
|||
}); |
|||
|
|||
it('should be valid green', () => { |
|||
const color = Color.GREEN; |
|||
|
|||
expect(color.r).toBe(0); |
|||
expect(color.g).toBe(1); |
|||
expect(color.b).toBe(0); |
|||
}); |
|||
|
|||
it('should be valid blue', () => { |
|||
const color = Color.BLUE; |
|||
|
|||
expect(color.r).toBe(0); |
|||
expect(color.g).toBe(0); |
|||
expect(color.b).toBe(1); |
|||
}); |
|||
|
|||
it('should calculate correct luminance', () => { |
|||
expect(new Color(1, 1, 1).luminance).toBe(1); |
|||
expect(new Color(0, 0, 0).luminance).toBe(0); |
|||
expect(new Color(1, 0, 0).luminance).toBe(1 / 3); |
|||
expect(new Color(0, 1, 0).luminance).toBe(1 / 2); |
|||
expect(new Color(0, 0, 1).luminance).toBe(1 / 6); |
|||
}); |
|||
|
|||
it('should make valid equal comparisons', () => { |
|||
expect(new Color(0.1, 0.1, 0.1).eq(new Color(0.1, 0.1, 0.1))).toBeTruthy(); |
|||
expect(new Color(0.1, 0.1, 0.4).eq(new Color(0.1, 0.1, 0.1))).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should make valid not equal comparisons', () => { |
|||
expect(new Color(0.1, 0.1, 0.1).ne(new Color(0.1, 0.1, 0.4))).toBeTruthy(); |
|||
expect(new Color(0.1, 0.1, 0.1).ne(new Color(0.1, 0.1, 0.1))).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should return color when creating from color', () => { |
|||
const color = Color.fromHsv(300, 1, 1); |
|||
const created = Color.fromValue(color); |
|||
|
|||
expect(created).toBe(color); |
|||
}); |
|||
|
|||
it('should throw error for invalid string', () => { |
|||
expect(() => Color.fromValue('INVALID')).toThrowError('Color is not in a valid format.'); |
|||
}); |
|||
}); |
|||
@ -1,200 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
/* tslint:disable:no-bitwise */ |
|||
|
|||
interface IColorDefinition { |
|||
regex: RegExp; |
|||
|
|||
process(bots: RegExpExecArray): Color; |
|||
} |
|||
|
|||
const ColorDefinitions: IColorDefinition[] = [ |
|||
{ |
|||
regex: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, |
|||
process: bits => new Color( |
|||
parseInt(bits[1], 10) / 255, |
|||
parseInt(bits[2], 10) / 255, |
|||
parseInt(bits[3], 10) / 255) |
|||
}, |
|||
{ |
|||
regex: /^(\w{2})(\w{2})(\w{2})$/, |
|||
process: bits => new Color( |
|||
parseInt(bits[1], 16) / 255, |
|||
parseInt(bits[2], 16) / 255, |
|||
parseInt(bits[3], 16) / 255) |
|||
}, |
|||
{ |
|||
regex: /^(\w{1})(\w{1})(\w{1})$/, |
|||
process: bits => new Color( |
|||
parseInt(bits[1] + bits[1], 16) / 255, |
|||
parseInt(bits[2] + bits[2], 16) / 255, |
|||
parseInt(bits[3] + bits[3], 16) / 255) |
|||
} |
|||
]; |
|||
|
|||
export class Color { |
|||
public static readonly BLACK = new Color(0, 0, 0); |
|||
public static readonly WHITE = new Color(1, 1, 1); |
|||
public static readonly GREEN = new Color(0, 1, 0); |
|||
public static readonly BLUE = new Color(0, 0, 1); |
|||
public static readonly RED = new Color(1, 0, 0); |
|||
|
|||
public readonly r: number; |
|||
public readonly g: number; |
|||
public readonly b: number; |
|||
|
|||
public get luminance(): number { |
|||
return (this.r + this.r + this.b + this.g + this.g + this.g) / 6; |
|||
} |
|||
|
|||
constructor(r: number, g: number, b: number) { |
|||
this.r = Math.min(1, Math.max(0, r)); |
|||
this.g = Math.min(1, Math.max(0, g)); |
|||
this.b = Math.min(1, Math.max(0, b)); |
|||
|
|||
Object.freeze(this); |
|||
} |
|||
|
|||
public eq(v: Color): boolean { |
|||
return this.r === v.r && this.g === v.g && this.b === v.b; |
|||
} |
|||
|
|||
public ne(v: Color): boolean { |
|||
return this.r !== v.r || this.g !== v.g || this.b !== v.b; |
|||
} |
|||
|
|||
public toNumber(): number { |
|||
return ((this.r * 255) << 16) + ((this.g * 255) << 8) + (this.b * 255); |
|||
} |
|||
|
|||
public toString(): string { |
|||
let r = Math.round(this.r * 255).toString(16); |
|||
let g = Math.round(this.g * 255).toString(16); |
|||
let b = Math.round(this.b * 255).toString(16); |
|||
|
|||
if (r.length === 1) { |
|||
r = '0' + r; |
|||
} |
|||
if (g.length === 1) { |
|||
g = '0' + g; |
|||
} |
|||
if (b.length === 1) { |
|||
b = '0' + b; |
|||
} |
|||
|
|||
return '#' + r + g + b; |
|||
} |
|||
|
|||
public static fromHex(r: number, g: number, b: number): Color { |
|||
return new Color( |
|||
parseInt('' + r, 16) / 255, |
|||
parseInt('' + g, 16) / 255, |
|||
parseInt('' + b, 16) / 255); |
|||
} |
|||
|
|||
public static fromNumber(rgb: number): Color { |
|||
return new Color( |
|||
((rgb >> 16) & 0xff) / 255, |
|||
((rgb >> 8) & 0xff) / 255, |
|||
((rgb >> 0) & 0xff) / 255); |
|||
} |
|||
|
|||
public static fromValue(value: number | string | Color): Color { |
|||
if (typeof value === 'string') { |
|||
return Color.fromString(value); |
|||
} else if (typeof value === 'number') { |
|||
return Color.fromNumber(value); |
|||
} else { |
|||
return value; |
|||
} |
|||
} |
|||
|
|||
public static fromString(value: string) { |
|||
if (value.charAt(0) === '#') { |
|||
value = value.substr(1, 6); |
|||
} |
|||
|
|||
value = value.replace(/ /g, '').toLowerCase(); |
|||
|
|||
for (let colorDefinition of ColorDefinitions) { |
|||
const bits = colorDefinition.regex.exec(value); |
|||
|
|||
if (bits) { |
|||
return colorDefinition.process(bits); |
|||
} |
|||
} |
|||
|
|||
throw new Error('Color is not in a valid format.'); |
|||
} |
|||
|
|||
public static fromHsv(h: number, s: number, v: number): Color { |
|||
const hi = Math.floor(h / 60) % 6; |
|||
|
|||
const f = (h / 60) - Math.floor(h / 60); |
|||
|
|||
v = v * 255; |
|||
|
|||
const p = (v * (1 - s)); |
|||
const q = (v * (1 - (f * s))); |
|||
const t = (v * (1 - ((1 - f) * s))); |
|||
|
|||
switch (hi) { |
|||
case 0: |
|||
return new Color(v, t, p); |
|||
case 1: |
|||
return new Color(q, v, p); |
|||
case 2: |
|||
return new Color(p, v, t); |
|||
case 3: |
|||
return new Color(p, q, v); |
|||
case 4: |
|||
return new Color(t, p, v); |
|||
default: |
|||
return new Color(v, p, q); |
|||
} |
|||
} |
|||
|
|||
public static fromHsl(h: number, s: number, l: number): Color { |
|||
let r = 0, g = 0, b = 0; |
|||
|
|||
h = h / 360; |
|||
|
|||
if (s === 0) { |
|||
r = g = b = l; |
|||
} else { |
|||
const hue2rgb = (p: number, q: number, t: number) => { |
|||
if (t < 0) { |
|||
t += 1; |
|||
} |
|||
|
|||
if (t > 1) { |
|||
t -= 1; |
|||
} |
|||
if (t < 1 / 6) { |
|||
return p + (q - p) * 6 * t; |
|||
} |
|||
if (t < 1 / 2) { |
|||
return q; |
|||
} |
|||
if (t < 2 / 3) { |
|||
return p + (q - p) * (2 / 3 - t) * 6; |
|||
} |
|||
return p; |
|||
}; |
|||
|
|||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s; |
|||
const p = 2 * l - q; |
|||
|
|||
r = hue2rgb(p, q, h + 1 / 3); |
|||
g = hue2rgb(p, q, h); |
|||
b = hue2rgb(p, q, h - 1 / 3); |
|||
} |
|||
|
|||
return new Color(r, g, b); |
|||
} |
|||
} |
|||
@ -1,237 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { |
|||
Rect2, |
|||
Rotation, |
|||
Vec2 |
|||
} from './../'; |
|||
|
|||
describe('Rect2', () => { |
|||
it('should provide values from constructor', () => { |
|||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|||
|
|||
expect(rect.x).toBe(10); |
|||
expect(rect.y).toBe(20); |
|||
expect(rect.top).toBe(20); |
|||
expect(rect.left).toBe(10); |
|||
expect(rect.right).toBe(60); |
|||
expect(rect.width).toBe(50); |
|||
expect(rect.height).toBe(30); |
|||
expect(rect.bottom).toBe(50); |
|||
expect(rect.centerX).toBe(35); |
|||
expect(rect.centerY).toBe(35); |
|||
|
|||
expect(rect.size).toEqual(new Vec2(50, 30)); |
|||
expect(rect.center).toEqual(new Vec2(35, 35)); |
|||
expect(rect.position).toEqual(new Vec2(10, 20)); |
|||
|
|||
expect(rect.area).toBe(1500); |
|||
|
|||
expect(rect.toString()).toBe('(x: 10, y: 20, w: 50, h: 30)'); |
|||
}); |
|||
|
|||
it('should calculate isEmpty correctly', () => { |
|||
expect(new Rect2(new Vec2(10, 20), new Vec2(50, 30)).isEmpty).toBeFalsy(); |
|||
expect(new Rect2(new Vec2(10, 20), new Vec2(-1, 30)).isEmpty).toBeTruthy(); |
|||
expect(new Rect2(new Vec2(10, 20), new Vec2(50, -9)).isEmpty).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should calculate isInfinite correctly', () => { |
|||
expect(new Rect2(new Vec2(10, 20), new Vec2(50, 30)).isInfinite).toBeFalsy(); |
|||
expect(new Rect2(new Vec2(10, 20), new Vec2(Number.POSITIVE_INFINITY, 30)).isInfinite).toBeTruthy(); |
|||
expect(new Rect2(new Vec2(10, 20), new Vec2(50, Number.POSITIVE_INFINITY)).isInfinite).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should inflate correctly', () => { |
|||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|||
|
|||
const actual1 = rect.inflateV(new Vec2(10, 20)); |
|||
const actual2 = rect.inflate(10, 20); |
|||
const expected = new Rect2(new Vec2(0, 0), new Vec2(70, 70)); |
|||
|
|||
expect(actual1).toEqual(expected); |
|||
expect(actual2).toEqual(expected); |
|||
}); |
|||
|
|||
it('should deflate correctly', () => { |
|||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|||
|
|||
const actual1 = rect.deflateV(new Vec2(25, 15)); |
|||
const actual2 = rect.deflate(25, 15); |
|||
const expected = new Rect2(new Vec2(35, 35), new Vec2(0, 0)); |
|||
|
|||
expect(actual1).toEqual(expected); |
|||
expect(actual2).toEqual(expected); |
|||
}); |
|||
|
|||
it('should return true for intersection with infinite rect', () => { |
|||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|||
|
|||
expect(rect.intersectsWith(Rect2.INFINITE)).toBeTruthy(); |
|||
|
|||
expect(Rect2.INFINITE.intersectsWith(rect)).toBeTruthy(); |
|||
expect(Rect2.INFINITE.intersectsWith(Rect2.INFINITE)).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should return false for intersection with empty rect', () => { |
|||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|||
|
|||
expect(rect.intersectsWith(Rect2.EMPTY)).toBeFalsy(); |
|||
|
|||
expect(Rect2.EMPTY.intersectsWith(rect)).toBeFalsy(); |
|||
expect(Rect2.EMPTY.intersectsWith(Rect2.INFINITE)).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should return empty for no intersection', () => { |
|||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|||
const outer = new Rect2(new Vec2(100, 20), new Vec2(50, 30)); |
|||
|
|||
const actual = rect.intersect(outer); |
|||
const expected = Rect2.EMPTY; |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should return result for intersection', () => { |
|||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|||
const inner = new Rect2(new Vec2(35, 35), new Vec2(100, 30)); |
|||
|
|||
const actual = rect.intersect(inner); |
|||
const expected = new Rect2(new Vec2(35, 35), new Vec2(25, 15)); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should make correct contains by vector tests', () => { |
|||
const rect = new Rect2(new Vec2(10, 20), new Vec2(50, 30)); |
|||
|
|||
expect(rect.containsVec(rect.center)).toBeTruthy(); |
|||
expect(rect.containsVec(new Vec2(rect.left, rect.top))).toBeTruthy(); |
|||
|
|||
expect(rect.containsVec(new Vec2(rect.centerX, 0))).toBeFalsy(); |
|||
expect(rect.containsVec(new Vec2(rect.centerX, 100))).toBeFalsy(); |
|||
expect(rect.containsVec(new Vec2(100, rect.centerY))).toBeFalsy(); |
|||
expect(rect.containsVec(new Vec2(-50, rect.centerY))).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should return true when rect contains other rect', () => { |
|||
const rect = new Rect2(new Vec2(400, 400), new Vec2(400, 400)); |
|||
const other = new Rect2(new Vec2(500, 500), new Vec2(200, 200)); |
|||
|
|||
expect(rect.containsRect(other)).toBeTruthy(); |
|||
}); |
|||
|
|||
it('should return false when other rect is too top', () => { |
|||
const rect = new Rect2(new Vec2(400, 400), new Vec2(400, 400)); |
|||
const other = new Rect2(new Vec2(300, 900), new Vec2(300, 100)); |
|||
|
|||
expect(rect.containsRect(other)).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should return false when other rect is too bottom', () => { |
|||
const rect = new Rect2(new Vec2(400, 400), new Vec2(400, 400)); |
|||
const other = new Rect2(new Vec2(300, 900), new Vec2(100, 300)); |
|||
|
|||
expect(rect.containsRect(other)).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should return false when other rect is too left', () => { |
|||
const rect = new Rect2(new Vec2(400, 400), new Vec2(400, 400)); |
|||
const other = new Rect2(new Vec2(200, 200), new Vec2(100, 300)); |
|||
|
|||
expect(rect.containsRect(other)).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should return false when other right is too left', () => { |
|||
const rect = new Rect2(new Vec2(400, 400), new Vec2(400, 400)); |
|||
const other = new Rect2(new Vec2(900, 200), new Vec2(100, 300)); |
|||
|
|||
expect(rect.containsRect(other)).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should return empty when creating from null vectors', () => { |
|||
const actual = Rect2.createFromVecs(null); |
|||
const expected = Rect2.EMPTY; |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should return empty when creating from null rects', () => { |
|||
const actual = Rect2.createFromRects(null); |
|||
const expected = Rect2.EMPTY; |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should provide valid zero instance', () => { |
|||
const actual = Rect2.ZERO; |
|||
const expected = new Rect2(new Vec2(0, 0), new Vec2(0, 0)); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should provide valid empty instance', () => { |
|||
const actual = Rect2.EMPTY; |
|||
const expected = new Rect2(Vec2.NEGATIVE_INFINITE, Vec2.NEGATIVE_INFINITE); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should provide valid infinite instance', () => { |
|||
const actual = Rect2.INFINITE; |
|||
const expected = new Rect2(Vec2.POSITIVE_INFINITE, Vec2.POSITIVE_INFINITE); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should create correct rect from vectors', () => { |
|||
const actual = |
|||
Rect2.createFromVecs([ |
|||
new Vec2(100, 100), |
|||
new Vec2(500, 300), |
|||
new Vec2(900, 800)]); |
|||
const expected = new Rect2(new Vec2(100, 100), new Vec2(800, 700)); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should create correct rect from rects', () => { |
|||
const actual = |
|||
Rect2.createFromRects([ |
|||
new Rect2(new Vec2(100, 100), new Vec2(100, 100)), |
|||
new Rect2(new Vec2(500, 300), new Vec2(100, 100)), |
|||
new Rect2(new Vec2(150, 150), new Vec2(750, 650))]); |
|||
const expected = new Rect2(new Vec2(100, 100), new Vec2(800, 700)); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should create rect from rotation', () => { |
|||
const actual = Rect2.createRotated(new Vec2(400, 300), new Vec2(600, 400), Rotation.createFromRadian(Math.PI / 2)); |
|||
const expected = new Rect2(new Vec2(500, 200), new Vec2(400, 600)); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should create rect from zero rotation', () => { |
|||
const actual = Rect2.createRotated(new Vec2(400, 300), new Vec2(600, 400), Rotation.ZERO); |
|||
const expected = new Rect2(new Vec2(400, 300), new Vec2(600, 400)); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should make valid equal comparisons', () => { |
|||
expect(new Rect2(new Vec2(10, 10), new Vec2(10, 10)).eq(new Rect2(new Vec2(10, 10), new Vec2(10, 10)))).toBeTruthy(); |
|||
expect(new Rect2(new Vec2(10, 10), new Vec2(10, 10)).eq(new Rect2(new Vec2(20, 20), new Vec2(20, 20)))).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should make valid not equal comparisons', () => { |
|||
expect(new Rect2(new Vec2(10, 10), new Vec2(10, 10)).ne(new Rect2(new Vec2(10, 10), new Vec2(10, 10)))).toBeFalsy(); |
|||
expect(new Rect2(new Vec2(10, 10), new Vec2(10, 10)).ne(new Rect2(new Vec2(20, 20), new Vec2(20, 20)))).toBeTruthy(); |
|||
}); |
|||
}); |
|||
@ -1,191 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Rotation } from './rotation'; |
|||
import { Vec2 } from './vec2'; |
|||
|
|||
export class Rect2 { |
|||
public static readonly ZERO = new Rect2(Vec2.ZERO, Vec2.ZERO); |
|||
public static readonly EMPTY = new Rect2(Vec2.NEGATIVE_INFINITE, Vec2.NEGATIVE_INFINITE); |
|||
public static readonly INFINITE = new Rect2(Vec2.POSITIVE_INFINITE, Vec2.POSITIVE_INFINITE); |
|||
|
|||
public get center(): Vec2 { |
|||
return new Vec2(this.position.x + (0.5 * this.size.x), this.position.y + (0.5 * this.size.y)); |
|||
} |
|||
|
|||
public get area(): number { |
|||
return this.size.x * this.size.y; |
|||
} |
|||
|
|||
public get x(): number { |
|||
return this.position.x; |
|||
} |
|||
|
|||
public get y(): number { |
|||
return this.position.y; |
|||
} |
|||
|
|||
public get left(): number { |
|||
return this.position.x; |
|||
} |
|||
|
|||
public get top(): number { |
|||
return this.position.y; |
|||
} |
|||
|
|||
public get right(): number { |
|||
return this.position.x + this.size.x; |
|||
} |
|||
|
|||
public get bottom(): number { |
|||
return this.position.y + this.size.y; |
|||
} |
|||
|
|||
public get width(): number { |
|||
return this.size.x; |
|||
} |
|||
|
|||
public get height(): number { |
|||
return this.size.y; |
|||
} |
|||
|
|||
public get centerX(): number { |
|||
return this.position.x + (0.5 * this.size.x); |
|||
} |
|||
|
|||
public get centerY(): number { |
|||
return this.position.y + (0.5 * this.size.y); |
|||
} |
|||
|
|||
public get isEmpty(): boolean { |
|||
return this.size.x < 0 || this.size.y < 0; |
|||
} |
|||
|
|||
public get isInfinite(): boolean { |
|||
return this.size.x === Number.POSITIVE_INFINITY || this.size.y === Number.POSITIVE_INFINITY; |
|||
} |
|||
|
|||
constructor( |
|||
public readonly position: Vec2, |
|||
public readonly size: Vec2 |
|||
) { |
|||
} |
|||
|
|||
public static createFromVecs(vecs: Vec2[] | null): Rect2 { |
|||
if (!vecs || vecs.length === 0) { |
|||
return Rect2.EMPTY; |
|||
} |
|||
|
|||
let minX = Number.MAX_VALUE; |
|||
let minY = Number.MAX_VALUE; |
|||
let maxX = Number.MIN_VALUE; |
|||
let maxY = Number.MIN_VALUE; |
|||
|
|||
for (let v of vecs) { |
|||
minX = Math.min(minX, v.x); |
|||
minY = Math.min(minY, v.y); |
|||
maxX = Math.max(maxX, v.x); |
|||
maxY = Math.max(maxY, v.y); |
|||
} |
|||
|
|||
return new Rect2(new Vec2(minX, minY), new Vec2(Math.max(0, maxX - minX), Math.max(0, maxY - minY))); |
|||
} |
|||
|
|||
public static createFromRects(rects: Rect2[] | null): Rect2 { |
|||
if (!rects || rects.length === 0) { |
|||
return Rect2.EMPTY; |
|||
} |
|||
|
|||
let minX = Number.MAX_VALUE; |
|||
let minY = Number.MAX_VALUE; |
|||
let maxX = Number.MIN_VALUE; |
|||
let maxY = Number.MIN_VALUE; |
|||
|
|||
for (let r of rects) { |
|||
minX = Math.min(minX, r.left); |
|||
minY = Math.min(minY, r.top); |
|||
maxX = Math.max(maxX, r.right); |
|||
maxY = Math.max(maxY, r.bottom); |
|||
} |
|||
|
|||
return new Rect2(new Vec2(minX, minY), new Vec2(Math.max(0, maxX - minX), Math.max(0, maxY - minY))); |
|||
} |
|||
|
|||
public static createRotated(position: Vec2, size: Vec2, rotation: Rotation): Rect2 { |
|||
const x = position.x; |
|||
const y = position.y; |
|||
const w = size.x; |
|||
const h = size.y; |
|||
|
|||
if (Math.abs(rotation.sin) < Number.EPSILON) { |
|||
return new Rect2(position, size); |
|||
} |
|||
|
|||
const center = new Vec2(x + (w * 0.5), y + (h * 0.5)); |
|||
|
|||
const lt = Vec2.createRotated(new Vec2(x + 0, y + 0), center, rotation); |
|||
const rt = Vec2.createRotated(new Vec2(x + w, y + 0), center, rotation); |
|||
const rb = Vec2.createRotated(new Vec2(x + w, y + h), center, rotation); |
|||
const lb = Vec2.createRotated(new Vec2(x + 0, y + h), center, rotation); |
|||
|
|||
return Rect2.createFromVecs([lb, lt, rb, rt]); |
|||
} |
|||
|
|||
public eq(r: Rect2): boolean { |
|||
return this.position.eq(r.position) && this.size.eq(r.size); |
|||
} |
|||
|
|||
public ne(r: Rect2): boolean { |
|||
return this.position.ne(r.position) || this.size.ne(r.size); |
|||
} |
|||
|
|||
public toString(): string { |
|||
return `(x: ${this.x}, y: ${this.y}, w: ${this.width}, h: ${this.height})`; |
|||
} |
|||
|
|||
public inflateV(v: Vec2): Rect2 { |
|||
return this.inflate(v.x, v.y); |
|||
} |
|||
|
|||
public inflate(w: number, h: number): Rect2 { |
|||
return new Rect2(new Vec2(this.position.x - w, this.position.y - h), new Vec2(this.size.x + (2 * w), this.size.y + (2 * h))); |
|||
} |
|||
|
|||
public deflateV(v: Vec2): Rect2 { |
|||
return this.deflate(v.x, v.y); |
|||
} |
|||
|
|||
public deflate(w: number, h: number): Rect2 { |
|||
return new Rect2(new Vec2(this.position.x + w, this.position.y + h), new Vec2(Math.max(0, this.size.x - (2 * w)), Math.max(0, this.size.y - (2 * h)))); |
|||
} |
|||
|
|||
public containsRect(r: Rect2): boolean { |
|||
return r.left >= this.left && r.right <= this.right && r.top >= this.top && r.bottom <= this.bottom; |
|||
} |
|||
|
|||
public containsVec(v: Vec2): boolean { |
|||
return v.x >= this.position.x && v.x - this.size.x <= this.position.x && v.y >= this.position.y && v.y - this.size.y <= this.position.y; |
|||
} |
|||
|
|||
public intersectsWith(r: Rect2): boolean { |
|||
return !this.isEmpty && !r.isEmpty && ((r.left <= this.right && r.right >= this.left && r.top <= this.bottom && r.bottom >= this.top) || this.isInfinite || r.isInfinite); |
|||
} |
|||
|
|||
public intersect(r: Rect2): Rect2 { |
|||
if (!this.intersectsWith(r)) { |
|||
return Rect2.EMPTY; |
|||
} |
|||
|
|||
const minX = Math.max(this.x, r.x); |
|||
const minY = Math.max(this.y, r.y); |
|||
|
|||
const w = Math.min(this.position.x + this.size.x, r.position.x + r.size.x) - minX; |
|||
const h = Math.min(this.position.y + this.size.y, r.position.y + r.size.y) - minY; |
|||
|
|||
return new Rect2(new Vec2(minX, minY), new Vec2(Math.max(w, 0.0), Math.max(h, 0.0))); |
|||
} |
|||
} |
|||
@ -1,73 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Rotation } from './../'; |
|||
|
|||
describe('Rotation', () => { |
|||
it('should sub correctly', () => { |
|||
const rotation1 = Rotation.createFromDegree(180); |
|||
const rotation2 = Rotation.createFromDegree(45); |
|||
|
|||
const actual = rotation1.sub(rotation2); |
|||
const expected = Rotation.createFromDegree(135); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should add correctly', () => { |
|||
const rotation1 = Rotation.createFromDegree(180); |
|||
const rotation2 = Rotation.createFromDegree(45); |
|||
|
|||
const actual = rotation1.add(rotation2); |
|||
const expected = Rotation.createFromDegree(225); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should calculate negated rotation', () => { |
|||
const rotation = Rotation.createFromDegree(180); |
|||
|
|||
const actual = rotation.negate(); |
|||
const expected = Rotation.createFromDegree(-180); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should create rotation by degree', () => { |
|||
const rotation = Rotation.createFromDegree(180); |
|||
|
|||
expect(rotation.degree).toBe(180); |
|||
expect(rotation.radian).toBe(Math.PI); |
|||
|
|||
expect(rotation.cos).toBe(Math.cos(Math.PI)); |
|||
expect(rotation.sin).toBe(Math.sin(Math.PI)); |
|||
|
|||
expect(rotation.toString()).toBe('180°'); |
|||
}); |
|||
|
|||
it('should create rotation by radian', () => { |
|||
const rotation = Rotation.createFromRadian(Math.PI); |
|||
|
|||
expect(rotation.degree).toBe(180); |
|||
expect(rotation.radian).toBe(Math.PI); |
|||
|
|||
expect(rotation.cos).toBe(Math.cos(Math.PI)); |
|||
expect(rotation.sin).toBe(Math.sin(Math.PI)); |
|||
|
|||
expect(rotation.toString()).toBe('180°'); |
|||
}); |
|||
|
|||
it('should make correct equal comparisons', () => { |
|||
expect(Rotation.createFromDegree(123).eq(Rotation.createFromDegree(123))).toBeTruthy(); |
|||
expect(Rotation.createFromDegree(123).eq(Rotation.createFromDegree(234))).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should make correct not equal comparisons', () => { |
|||
expect(Rotation.createFromDegree(123).ne(Rotation.createFromDegree(123))).toBeFalsy(); |
|||
expect(Rotation.createFromDegree(123).ne(Rotation.createFromDegree(234))).toBeTruthy(); |
|||
}); |
|||
}); |
|||
@ -1,57 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { MathHelper } from './math-helper'; |
|||
|
|||
export class Rotation { |
|||
public static readonly ZERO = Rotation.createFromRadian(0); |
|||
|
|||
public cos: number; |
|||
public sin: number; |
|||
|
|||
constructor( |
|||
public readonly radian: number, |
|||
public readonly degree: number |
|||
) { |
|||
this.cos = Math.cos(radian); |
|||
this.sin = Math.sin(radian); |
|||
|
|||
Object.freeze(this); |
|||
} |
|||
|
|||
public static createFromRadian(radian: number): Rotation { |
|||
return new Rotation(radian, MathHelper.toDegree(radian)); |
|||
} |
|||
|
|||
public static createFromDegree(degree: number): Rotation { |
|||
return new Rotation(MathHelper.toRad(degree), degree); |
|||
} |
|||
|
|||
public eq(r: Rotation): boolean { |
|||
return this.radian === r.radian; |
|||
} |
|||
|
|||
public ne(r: Rotation): boolean { |
|||
return this.radian !== r.radian; |
|||
} |
|||
|
|||
public toString(): string { |
|||
return `${this.degree}°`; |
|||
} |
|||
|
|||
public add(r: Rotation): Rotation { |
|||
return Rotation.createFromDegree(MathHelper.toPositiveDegree(this.degree + r.degree)); |
|||
} |
|||
|
|||
public sub(r: Rotation): Rotation { |
|||
return Rotation.createFromDegree(MathHelper.toPositiveDegree(this.degree - r.degree)); |
|||
} |
|||
|
|||
public negate(): Rotation { |
|||
return Rotation.createFromDegree(-this.degree); |
|||
} |
|||
} |
|||
@ -1,166 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { Rotation, Vec2 } from './../'; |
|||
|
|||
describe('Vec2', () => { |
|||
it('should instantiate from x and y value', () => { |
|||
const v = new Vec2(10, 20); |
|||
|
|||
expect(v.x).toBe(10); |
|||
expect(v.y).toBe(20); |
|||
|
|||
expect(v.toString()).toBe('(10, 20)'); |
|||
}); |
|||
|
|||
it('should make valid equal comparisons', () => { |
|||
expect(new Vec2(10, 10).eq(new Vec2(10, 10))).toBeTruthy(); |
|||
expect(new Vec2(10, 10).eq(new Vec2(20, 20))).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should make valid not equal comparisons', () => { |
|||
expect(new Vec2(10, 10).ne(new Vec2(20, 20))).toBeTruthy(); |
|||
expect(new Vec2(10, 10).ne(new Vec2(10, 10))).toBeFalsy(); |
|||
}); |
|||
|
|||
it('should round with default value', () => { |
|||
const actual = new Vec2(1.3, 1.6).round(); |
|||
const expected = new Vec2(1, 2); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should calculate multiple of 10', () => { |
|||
const actual = new Vec2(13, 16).round(10); |
|||
const expected = new Vec2(10, 20); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should calculate multiple of 2', () => { |
|||
const actual = new Vec2(13, 12.2).roundToMultipleOfTwo(); |
|||
const expected = new Vec2(14, 12); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should add by vector correctly', () => { |
|||
const actual = new Vec2(15, 12).add(new Vec2(4, 1)); |
|||
const expected = new Vec2(19, 13); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should add by scalar correctly', () => { |
|||
const actual = new Vec2(15, 12).addScalar(3); |
|||
const expected = new Vec2(18, 15); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should subtract by vector correctly', () => { |
|||
const actual = new Vec2(15, 12).sub(new Vec2(4, 1)); |
|||
const expected = new Vec2(11, 11); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should subtract by scalar correctly', () => { |
|||
const actual = new Vec2(15, 12).subScalar(3); |
|||
const expected = new Vec2(12, 9); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should multiply by vector correctly', () => { |
|||
const actual = new Vec2(15, 12).mul(new Vec2(4, 2)); |
|||
const expected = new Vec2(60, 24); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should multiply by scalar correctly', () => { |
|||
const actual = new Vec2(15, 12).mulScalar(3); |
|||
const expected = new Vec2(45, 36); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should divide by vector correctly', () => { |
|||
const actual = new Vec2(15, 12).div(new Vec2(5, 2)); |
|||
const expected = new Vec2(3, 6); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should divide by scalar correctly', () => { |
|||
const actual = new Vec2(15, 12).divScalar(3); |
|||
const expected = new Vec2(5, 4); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should negate correctly', () => { |
|||
const actual = new Vec2(15, 12).negate(); |
|||
const expected = new Vec2(-15, -12); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should calculate max vector', () => { |
|||
const actual = Vec2.max(new Vec2(20, 10), new Vec2(15, 30)); |
|||
const expected = new Vec2(20, 30); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should calculate min vector', () => { |
|||
const actual = Vec2.min(new Vec2(20, 10), new Vec2(15, 30)); |
|||
const expected = new Vec2(15, 10); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should calculate length', () => { |
|||
const actual = new Vec2(10, 10).length; |
|||
const expected = Math.sqrt(200); |
|||
|
|||
expect(actual).toBe(expected); |
|||
}); |
|||
|
|||
it('should calculate length squared', () => { |
|||
const actual = new Vec2(10, 10).lengtSquared; |
|||
const expected = 200; |
|||
|
|||
expect(actual).toBe(expected); |
|||
}); |
|||
|
|||
it('should calculate median', () => { |
|||
const actual = Vec2.createMedian(new Vec2(10, 20), new Vec2(20, 20), new Vec2(60, 20)); |
|||
const expected = new Vec2(30, 20); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should calculate rotated vector correctly', () => { |
|||
const source = new Vec2(40, 20); |
|||
const center = new Vec2(20, 20); |
|||
|
|||
const rotation = Rotation.createFromRadian(Math.PI / 2); |
|||
|
|||
const actual = Vec2.createRotated(source, center, rotation); |
|||
const expected = new Vec2(20, 40); |
|||
|
|||
expect(actual).toEqual(expected); |
|||
}); |
|||
|
|||
it('should calculate angles between vectors', () => { |
|||
expect(Vec2.angleBetween(new Vec2(1, 0), new Vec2(1, 0))).toBe(0); |
|||
expect(Vec2.angleBetween(new Vec2(1, 0), new Vec2(1, 1))).toBe(45); |
|||
expect(Vec2.angleBetween(new Vec2(1, 0), new Vec2(0, 1))).toBe(90); |
|||
}); |
|||
}); |
|||
@ -1,127 +0,0 @@ |
|||
/* |
|||
* Squidex Headless CMS |
|||
* |
|||
* @license |
|||
* Copyright (c) Sebastian Stehle. All rights reserved |
|||
*/ |
|||
|
|||
import { MathHelper } from './math-helper'; |
|||
import { Rotation } from './rotation'; |
|||
|
|||
export class Vec2 { |
|||
public static readonly ZERO = new Vec2(0, 0); |
|||
|
|||
public static readonly ONE = new Vec2(1, 1); |
|||
|
|||
public static readonly POSITIVE_INFINITE = new Vec2(Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); |
|||
public static readonly NEGATIVE_INFINITE = new Vec2(Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY); |
|||
|
|||
public get length(): number { |
|||
return Math.sqrt(this.x * this.x + this.y * this.y); |
|||
} |
|||
|
|||
public get lengtSquared(): number { |
|||
return this.x * this.x + this.y * this.y; |
|||
} |
|||
|
|||
constructor( |
|||
public readonly x: number, |
|||
public readonly y: number |
|||
) { |
|||
Object.freeze(this); |
|||
} |
|||
|
|||
public eq(v: Vec2): boolean { |
|||
return this.x === v.x && this.y === v.y; |
|||
} |
|||
|
|||
public ne(v: Vec2): boolean { |
|||
return this.x !== v.x || this.y !== v.y; |
|||
} |
|||
|
|||
public toString(): string { |
|||
return `(${this.x}, ${this.y})`; |
|||
} |
|||
|
|||
public add(v: Vec2): Vec2 { |
|||
return new Vec2(this.x + v.x, this.y + v.y); |
|||
} |
|||
|
|||
public addScalar(s: number): Vec2 { |
|||
return new Vec2(this.x + s, this.y + s); |
|||
} |
|||
|
|||
public sub(v: Vec2): Vec2 { |
|||
return new Vec2(this.x - v.x, this.y - v.y); |
|||
} |
|||
|
|||
public subScalar(s: number): Vec2 { |
|||
return new Vec2(this.x - s, this.y - s); |
|||
} |
|||
|
|||
public mul(v: Vec2): Vec2 { |
|||
return new Vec2(this.x * v.x, this.y * v.y); |
|||
} |
|||
|
|||
public mulScalar(s: number): Vec2 { |
|||
return new Vec2(this.x * s, this.y * s); |
|||
} |
|||
|
|||
public div(v: Vec2): Vec2 { |
|||
return new Vec2(this.x / v.x, this.y / v.y); |
|||
} |
|||
|
|||
public divScalar(s: number): Vec2 { |
|||
return new Vec2(this.x / s, this.y / s); |
|||
} |
|||
|
|||
public negate(): Vec2 { |
|||
return new Vec2(-this.x, -this.y); |
|||
} |
|||
|
|||
public static max(lhs: Vec2, rhs: Vec2): Vec2 { |
|||
return new Vec2(Math.max(lhs.x, rhs.x), Math.max(lhs.y, rhs.y)); |
|||
} |
|||
|
|||
public static min(lhs: Vec2, rhs: Vec2): Vec2 { |
|||
return new Vec2(Math.min(lhs.x, rhs.x), Math.min(lhs.y, rhs.y)); |
|||
} |
|||
|
|||
public round(factor = 1): Vec2 { |
|||
return new Vec2(MathHelper.roundToMultipleOf(this.x, factor), MathHelper.roundToMultipleOf(this.y, factor)); |
|||
} |
|||
|
|||
public roundToMultipleOfTwo(): Vec2 { |
|||
return new Vec2(MathHelper.roundToMultipleOf(this.x, 2), MathHelper.roundToMultipleOf(this.y, 2)); |
|||
} |
|||
|
|||
public static createRotated(vec: Vec2, center: Vec2, rotation: Rotation): Vec2 { |
|||
const x = vec.x - center.x; |
|||
const y = vec.y - center.y; |
|||
|
|||
const result = new Vec2( |
|||
(x * rotation.cos) - (y * rotation.sin) + center.x, |
|||
(x * rotation.sin) + (y * rotation.cos) + center.y); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public static createMedian(...vecs: Vec2[]) { |
|||
let medianX = 0; |
|||
let medianY = 0; |
|||
|
|||
for (let v of vecs) { |
|||
medianX += v.x; |
|||
medianY += v.y; |
|||
} |
|||
|
|||
return new Vec2(medianX / vecs.length, medianY / vecs.length); |
|||
} |
|||
|
|||
public static angleBetween(lhs: Vec2, rhs: Vec2): number { |
|||
const y = (lhs.x * rhs.y) - (rhs.x * lhs.y); |
|||
const x = (lhs.x * rhs.x) + (lhs.y * rhs.y); |
|||
|
|||
return MathHelper.toPositiveDegree(MathHelper.toDegree(Math.atan2(y, x))); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue