@rocon/balcan 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/bitmap.balcan.d.ts +4 -0
- package/dist/core/bitmap.balcan.js +22 -0
- package/dist/core/core.balcan.d.ts +73 -0
- package/dist/core/core.balcan.js +136 -0
- package/dist/core/geo.balcan.d.ts +109 -0
- package/dist/core/geo.balcan.js +255 -0
- package/dist/core/math.balcan.d.ts +35 -0
- package/dist/core/math.balcan.js +90 -0
- package/dist/core/ts-matrix/Matrix.d.ts +111 -0
- package/dist/core/ts-matrix/Matrix.js +243 -0
- package/dist/core/ts-matrix/Quat.d.ts +168 -0
- package/dist/core/ts-matrix/Quat.js +435 -0
- package/dist/core/ts-matrix/Vector.d.ts +145 -0
- package/dist/core/ts-matrix/Vector.js +252 -0
- package/dist/core/ts-matrix/constants.d.ts +1 -0
- package/dist/core/ts-matrix/constants.js +1 -0
- package/dist/core/ts-matrix/ts-matrix.d.ts +3 -0
- package/dist/core/ts-matrix/ts-matrix.js +31 -0
- package/dist/core/types.balcan.d.ts +93 -0
- package/dist/core/types.balcan.js +1 -0
- package/dist/core/util.balcan.d.ts +23 -0
- package/dist/core/util.balcan.js +71 -0
- package/dist/core/viewport.balcan.d.ts +27 -0
- package/dist/core/viewport.balcan.js +60 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +3 -0
- package/dist/mixin/mixin.d.ts +7 -0
- package/dist/mixin/mixin.js +7 -0
- package/dist/mixin/mixin.type.d.ts +2 -0
- package/dist/mixin/mixin.type.js +1 -0
- package/dist/staffs/image.staff.d.ts +27 -0
- package/dist/staffs/image.staff.js +74 -0
- package/dist/staffs/index.d.ts +3 -0
- package/dist/staffs/index.js +3 -0
- package/dist/staffs/wheelZoom.staff.d.ts +21 -0
- package/dist/staffs/wheelZoom.staff.js +45 -0
- package/dist/staffs/windowResizeObserver.staff.d.ts +8 -0
- package/dist/staffs/windowResizeObserver.staff.js +17 -0
- package/dist/types/geometry.d.ts +68 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +39 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export var bitmap;
|
|
2
|
+
(function (bitmap) {
|
|
3
|
+
/** data URL 이미지를 상하 반전한 새 data URL 반환 */
|
|
4
|
+
function flipVertical(dataUrl) {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const img = new Image();
|
|
7
|
+
img.onload = () => {
|
|
8
|
+
const canvas = document.createElement('canvas');
|
|
9
|
+
canvas.width = img.naturalWidth;
|
|
10
|
+
canvas.height = img.naturalHeight;
|
|
11
|
+
const ctx = canvas.getContext('2d');
|
|
12
|
+
ctx.translate(0, canvas.height);
|
|
13
|
+
ctx.scale(1, -1);
|
|
14
|
+
ctx.drawImage(img, 0, 0);
|
|
15
|
+
resolve(canvas.toDataURL('image/png'));
|
|
16
|
+
};
|
|
17
|
+
img.onerror = () => reject(new Error('Failed to load image for flip'));
|
|
18
|
+
img.src = dataUrl;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
bitmap.flipVertical = flipVertical;
|
|
22
|
+
})(bitmap || (bitmap = {}));
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type Konva from 'konva';
|
|
2
|
+
import type { KonvaEventObject } from 'konva/lib/Node';
|
|
3
|
+
import { Subject } from 'rxjs';
|
|
4
|
+
import type { Scene, Actor, Staff } from './types.balcan';
|
|
5
|
+
export declare function useBalcan(): {
|
|
6
|
+
createDirector: (name: string, stage: Konva.Stage) => Director | undefined;
|
|
7
|
+
directors: Readonly<{
|
|
8
|
+
[k: string]: Director;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
export declare class Director {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly stage: Konva.Stage;
|
|
14
|
+
scenes: {
|
|
15
|
+
[k: string]: Scene;
|
|
16
|
+
};
|
|
17
|
+
actors: {
|
|
18
|
+
[k: string]: Actor;
|
|
19
|
+
};
|
|
20
|
+
staffs: {
|
|
21
|
+
[k: string]: Staff;
|
|
22
|
+
};
|
|
23
|
+
constructor(name: string, stage: Konva.Stage);
|
|
24
|
+
private _doubleClickEvent;
|
|
25
|
+
onScaleChanged$: Subject<{
|
|
26
|
+
scale: number;
|
|
27
|
+
}>;
|
|
28
|
+
onMouseDoubleClicked$: Subject<KonvaEventObject<MouseEvent, import("konva/lib/Stage").Stage>>;
|
|
29
|
+
onKeyDown$: Subject<KeyboardEvent>;
|
|
30
|
+
addScene(scene: Scene): {
|
|
31
|
+
type: any;
|
|
32
|
+
uniqueId: string;
|
|
33
|
+
data: any;
|
|
34
|
+
director: import("./types.balcan").Director;
|
|
35
|
+
};
|
|
36
|
+
removeScene(uniqueId: string): void;
|
|
37
|
+
removeAllScenes(): void;
|
|
38
|
+
addActor(actor: Actor): {
|
|
39
|
+
type: any;
|
|
40
|
+
entityId: string;
|
|
41
|
+
uniqueId: string;
|
|
42
|
+
data: any;
|
|
43
|
+
director?: import("./types.balcan").Director;
|
|
44
|
+
plugins: import("./types.balcan").Plugin[];
|
|
45
|
+
addPlugin?: (plugin: import("./types.balcan").Plugin) => void;
|
|
46
|
+
removePlugin?: (pluginKey: string) => void;
|
|
47
|
+
onDestroy?: () => void;
|
|
48
|
+
onBeforeRender?: () => void;
|
|
49
|
+
onRender?: () => void;
|
|
50
|
+
onUpdate?: () => void;
|
|
51
|
+
};
|
|
52
|
+
changeActorId(oldId: string, newId: string): void;
|
|
53
|
+
removeActor(uniqueId: string): void;
|
|
54
|
+
removeAllActors(): void;
|
|
55
|
+
addStaff(staff: Staff): {
|
|
56
|
+
type: any;
|
|
57
|
+
entityId: string;
|
|
58
|
+
uniqueId: string;
|
|
59
|
+
data: any;
|
|
60
|
+
director?: import("./types.balcan").Director;
|
|
61
|
+
plugins: import("./types.balcan").Plugin[];
|
|
62
|
+
addPlugin?: (plugin: import("./types.balcan").Plugin) => void;
|
|
63
|
+
removePlugin?: (pluginKey: string) => void;
|
|
64
|
+
onDestroy?: () => void;
|
|
65
|
+
onBeforeRender?: () => void;
|
|
66
|
+
onRender?: () => void;
|
|
67
|
+
};
|
|
68
|
+
removeAllStaffs(): void;
|
|
69
|
+
removeStaff(uniqueId: string): void;
|
|
70
|
+
get scale(): number;
|
|
71
|
+
set scale(scale: number);
|
|
72
|
+
render(): void;
|
|
73
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Subject } from 'rxjs';
|
|
2
|
+
function balcanStore() {
|
|
3
|
+
const _directors = {};
|
|
4
|
+
function addDirector(director) {
|
|
5
|
+
_directors[director.name] = director;
|
|
6
|
+
}
|
|
7
|
+
return {
|
|
8
|
+
directors: _directors,
|
|
9
|
+
addDirector,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const $store = balcanStore();
|
|
13
|
+
export function useBalcan() {
|
|
14
|
+
return {
|
|
15
|
+
createDirector,
|
|
16
|
+
directors: $store.directors,
|
|
17
|
+
};
|
|
18
|
+
function createDirector(name, stage) {
|
|
19
|
+
if (!stage) {
|
|
20
|
+
console.error('Konva Stage is required to create a new stage.');
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const director = new Director(name, stage);
|
|
24
|
+
$store.addDirector(director);
|
|
25
|
+
return director;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export class Director {
|
|
29
|
+
name;
|
|
30
|
+
stage;
|
|
31
|
+
scenes;
|
|
32
|
+
actors;
|
|
33
|
+
staffs;
|
|
34
|
+
constructor(name, stage) {
|
|
35
|
+
this.name = name;
|
|
36
|
+
this.stage = stage;
|
|
37
|
+
this.staffs = {};
|
|
38
|
+
this.scenes = {};
|
|
39
|
+
this.actors = {};
|
|
40
|
+
this._doubleClickEvent();
|
|
41
|
+
stage.container().addEventListener('keydown', (e) => {
|
|
42
|
+
this.onKeyDown$.next(e);
|
|
43
|
+
});
|
|
44
|
+
stage.on('click', () => {
|
|
45
|
+
stage.container().tabIndex = 1;
|
|
46
|
+
stage.container().focus();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
_doubleClickEvent() {
|
|
50
|
+
this.stage.on('dblclick', (e) => {
|
|
51
|
+
this.onMouseDoubleClicked$.next(e);
|
|
52
|
+
this.stage.off('dblclick');
|
|
53
|
+
setTimeout(() => this._doubleClickEvent(), 400);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
onScaleChanged$ = new Subject();
|
|
57
|
+
onMouseDoubleClicked$ = new Subject();
|
|
58
|
+
onKeyDown$ = new Subject();
|
|
59
|
+
addScene(scene) {
|
|
60
|
+
if (this.scenes[scene.uniqueId]) {
|
|
61
|
+
console.error('Scene with the same uniqueId already exists.');
|
|
62
|
+
return this.scenes[scene.uniqueId];
|
|
63
|
+
}
|
|
64
|
+
this.scenes[scene.uniqueId] = scene;
|
|
65
|
+
return scene;
|
|
66
|
+
}
|
|
67
|
+
removeScene(uniqueId) {
|
|
68
|
+
// this.scenes[uniqueId]?.onDestroy?.();
|
|
69
|
+
delete this.scenes[uniqueId];
|
|
70
|
+
}
|
|
71
|
+
removeAllScenes() {
|
|
72
|
+
Object.keys(this.scenes).forEach((k) => {
|
|
73
|
+
this.removeScene(k);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
addActor(actor) {
|
|
77
|
+
this.actors[actor.uniqueId] = actor;
|
|
78
|
+
return actor;
|
|
79
|
+
}
|
|
80
|
+
changeActorId(oldId, newId) {
|
|
81
|
+
const actor = this.actors[oldId];
|
|
82
|
+
if (!actor)
|
|
83
|
+
return;
|
|
84
|
+
actor.uniqueId = newId;
|
|
85
|
+
this.actors[newId] = actor;
|
|
86
|
+
delete this.actors[oldId];
|
|
87
|
+
}
|
|
88
|
+
removeActor(uniqueId) {
|
|
89
|
+
this.actors[uniqueId]?.onDestroy?.();
|
|
90
|
+
delete this.actors[uniqueId];
|
|
91
|
+
}
|
|
92
|
+
removeAllActors() {
|
|
93
|
+
Object.keys(this.actors).forEach((k) => {
|
|
94
|
+
this.removeActor(k);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
addStaff(staff) {
|
|
98
|
+
this.staffs[staff.uniqueId] = staff;
|
|
99
|
+
return staff;
|
|
100
|
+
}
|
|
101
|
+
removeAllStaffs() {
|
|
102
|
+
Object.keys(this.staffs).forEach((k) => {
|
|
103
|
+
this.removeStaff(k);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
removeStaff(uniqueId) {
|
|
107
|
+
this.staffs[uniqueId]?.onDestroy?.();
|
|
108
|
+
delete this.staffs[uniqueId];
|
|
109
|
+
}
|
|
110
|
+
get scale() {
|
|
111
|
+
return this.stage.scale().x;
|
|
112
|
+
}
|
|
113
|
+
set scale(scale) {
|
|
114
|
+
this.stage.scale({
|
|
115
|
+
x: scale,
|
|
116
|
+
y: scale,
|
|
117
|
+
});
|
|
118
|
+
this.onScaleChanged$.next({ scale });
|
|
119
|
+
}
|
|
120
|
+
render() {
|
|
121
|
+
for (const uniqueId in this.staffs) {
|
|
122
|
+
const staff = this.staffs[uniqueId];
|
|
123
|
+
staff.onBeforeRender?.();
|
|
124
|
+
staff.plugins.forEach((x) => x.onBeforeRender?.());
|
|
125
|
+
staff.onRender?.();
|
|
126
|
+
staff.plugins.forEach((x) => x.onRender?.());
|
|
127
|
+
}
|
|
128
|
+
for (const uniqueId in this.actors) {
|
|
129
|
+
const actor = this.actors[uniqueId];
|
|
130
|
+
actor.onBeforeRender?.();
|
|
131
|
+
actor.plugins.forEach((x) => x.onBeforeRender?.());
|
|
132
|
+
actor.onRender?.();
|
|
133
|
+
actor.plugins.forEach((x) => x.onRender?.());
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export declare namespace geo {
|
|
2
|
+
function addVector(v1: BalcanGeo.Vector2, v2: BalcanGeo.Vector2): {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
};
|
|
6
|
+
function subtractVector(v1: BalcanGeo.Vector2, v2: BalcanGeo.Vector2): {
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
};
|
|
10
|
+
function multiplyVector(v: BalcanGeo.Vector2, factor: number): {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
};
|
|
14
|
+
function divideVector(v: BalcanGeo.Vector2, factor: number): BalcanGeo.Vector2;
|
|
15
|
+
/** 벡터들 에 회전 행렬 적용 */
|
|
16
|
+
function rotateVectors(params: {
|
|
17
|
+
points: BalcanGeo.Vector2[];
|
|
18
|
+
rotateAngle: number;
|
|
19
|
+
angleUnit?: 'degree' | 'radian';
|
|
20
|
+
/** 중심점 */
|
|
21
|
+
centerPosition?: BalcanGeo.Vector2;
|
|
22
|
+
invertY?: boolean;
|
|
23
|
+
}): {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
}[];
|
|
27
|
+
/**
|
|
28
|
+
* 어떤 점 P에서 어떤 선분 S에 수선을 내렸을 때, 그 수선의 발 X를 구함
|
|
29
|
+
* @param point 점 위치벡터 P
|
|
30
|
+
* @param segmentPosition 선분 S의 양 끝점 좌표
|
|
31
|
+
* @returns 수선의 발 X점의 좌표벡터
|
|
32
|
+
*/
|
|
33
|
+
function nearestPointOnSegment(point: BalcanGeo.Vector2, segmentPosition: [
|
|
34
|
+
BalcanGeo.Vector2 | BalcanGeo.Pose,
|
|
35
|
+
BalcanGeo.Vector2 | BalcanGeo.Pose
|
|
36
|
+
]): BalcanGeo.Vector2;
|
|
37
|
+
/**
|
|
38
|
+
* 주어진 점이 다각형 내부에 있는지 확인
|
|
39
|
+
*/
|
|
40
|
+
function isPointInPolygon(point: BalcanGeo.Vector2, polygon: BalcanGeo.Vector2[]): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* 선분을 주어진 길이만큼 양쪽을 깎는다.
|
|
43
|
+
*/
|
|
44
|
+
function trimSegment(params: {
|
|
45
|
+
/** 선분점 1 */
|
|
46
|
+
point1: BalcanGeo.Vector2;
|
|
47
|
+
/** 선분점 2 */
|
|
48
|
+
point2: BalcanGeo.Vector2;
|
|
49
|
+
/** 선분점 1을 단축할 길이 양 */
|
|
50
|
+
shortenLength1?: number;
|
|
51
|
+
/** 선분점 2를 단축할 길이 양 */
|
|
52
|
+
shortenLength2?: number;
|
|
53
|
+
}): {
|
|
54
|
+
point1: {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
};
|
|
58
|
+
point2: {
|
|
59
|
+
x: number;
|
|
60
|
+
y: number;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* 실좌표계 상 위치를 화면좌표계로 변환
|
|
65
|
+
* @param pose
|
|
66
|
+
* @param origin 원점을 삼으려는 화면좌표계 상 좌표
|
|
67
|
+
* @param pixelPerMeter 실좌표계의 1미터당 픽셀 수
|
|
68
|
+
*/
|
|
69
|
+
function transformPose(pose: BalcanGeo.Pose, origin: BalcanGeo.Vector2, pixelPerMeter: number): BalcanGeo.Position;
|
|
70
|
+
/**
|
|
71
|
+
* 화면좌표계상 위치를 실좌표계로 변환
|
|
72
|
+
* @param position
|
|
73
|
+
* @param realOrigin 원점을 삼으려는 화면좌표계 상 좌표
|
|
74
|
+
* @param pixelPerMeter 실좌표계의 1미터당 픽셀 수
|
|
75
|
+
*/
|
|
76
|
+
function transformPosition(position: BalcanGeo.Position, realOrigin: BalcanGeo.Vector2, pixelPerMeter: number): BalcanGeo.Pose;
|
|
77
|
+
/**
|
|
78
|
+
* 화면좌표계에서 viewport 관련 유용한 벡터들 계산
|
|
79
|
+
*/
|
|
80
|
+
function calcViewportVectors(opt: {
|
|
81
|
+
/** 캔버스의 사이즈 */
|
|
82
|
+
viewportSize: BalcanGeo.Vector2;
|
|
83
|
+
/** 계산을 원하는 참조점의 위치 벡터 */
|
|
84
|
+
referenceVector: BalcanGeo.Vector2;
|
|
85
|
+
/** Canvas 확대 배율 */
|
|
86
|
+
cameraZoomScale: number;
|
|
87
|
+
}): {
|
|
88
|
+
/** 참조점이 viewport 중앙으로 가는 벡터 */
|
|
89
|
+
toViewportCenter: {
|
|
90
|
+
x: number;
|
|
91
|
+
y: number;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* originPoint 와 endPoint 를 잇는 선분을 가정하고
|
|
96
|
+
* extendLength 만큼 양쪽으로 늘이거나 줄인 선분을 반환
|
|
97
|
+
* (extendLength는 음수일 수도 있음)
|
|
98
|
+
*
|
|
99
|
+
* @returns { origin: BalcanGeo.Vector2, end: BalcanGeo.Vector2 }
|
|
100
|
+
*/
|
|
101
|
+
function extendSegment(params: {
|
|
102
|
+
originPoint: BalcanGeo.Vector2;
|
|
103
|
+
endPoint: BalcanGeo.Vector2;
|
|
104
|
+
extendLength: number;
|
|
105
|
+
}): {
|
|
106
|
+
origin: BalcanGeo.Vector2;
|
|
107
|
+
end: BalcanGeo.Vector2;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { range } from 'lodash';
|
|
2
|
+
import { math } from './math.balcan';
|
|
3
|
+
import { Matrix } from './ts-matrix/ts-matrix';
|
|
4
|
+
export var geo;
|
|
5
|
+
(function (geo) {
|
|
6
|
+
function addVector(v1, v2) {
|
|
7
|
+
return {
|
|
8
|
+
x: v1.x + v2.x,
|
|
9
|
+
y: v1.y + v2.y,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
geo.addVector = addVector;
|
|
13
|
+
function subtractVector(v1, v2) {
|
|
14
|
+
return {
|
|
15
|
+
x: v1.x - v2.x,
|
|
16
|
+
y: v1.y - v2.y,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
geo.subtractVector = subtractVector;
|
|
20
|
+
function multiplyVector(v, factor) {
|
|
21
|
+
return {
|
|
22
|
+
x: v.x * factor,
|
|
23
|
+
y: v.y * factor,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
geo.multiplyVector = multiplyVector;
|
|
27
|
+
function divideVector(v, factor) {
|
|
28
|
+
if (factor === 0) {
|
|
29
|
+
console.error('divideVector: divide by zero');
|
|
30
|
+
return v;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
x: v.x / factor,
|
|
34
|
+
y: v.y / factor,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
geo.divideVector = divideVector;
|
|
38
|
+
/** 벡터들 에 회전 행렬 적용 */
|
|
39
|
+
function rotateVectors(params) {
|
|
40
|
+
const { x: centerX = 0, y: centerY = 0 } = params?.centerPosition ?? {
|
|
41
|
+
x: 0,
|
|
42
|
+
y: 0,
|
|
43
|
+
};
|
|
44
|
+
const rad = params?.angleUnit === 'degree'
|
|
45
|
+
? math.radian(params.rotateAngle, { invertY: params?.invertY })
|
|
46
|
+
: params.rotateAngle;
|
|
47
|
+
const rotationMatrix = new Matrix(2, 2, [
|
|
48
|
+
[Math.cos(rad), -Math.sin(rad)],
|
|
49
|
+
[Math.sin(rad), Math.cos(rad)],
|
|
50
|
+
]);
|
|
51
|
+
// 회전중심점과 회전할 점의 거리를 벡터로 표현
|
|
52
|
+
const vectorMatrix = new Matrix(2, params.points.length, [
|
|
53
|
+
params.points.map((p) => p.x - centerX),
|
|
54
|
+
params.points.map((p) => p.y - centerY),
|
|
55
|
+
]);
|
|
56
|
+
const rotatedMatrix = rotationMatrix.multiply(vectorMatrix);
|
|
57
|
+
const results = range(rotatedMatrix.columns).map((m) => {
|
|
58
|
+
return {
|
|
59
|
+
x: rotatedMatrix.values[0][m] + centerX,
|
|
60
|
+
y: rotatedMatrix.values[1][m] + centerY,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
geo.rotateVectors = rotateVectors;
|
|
66
|
+
/**
|
|
67
|
+
* 어떤 점 P에서 어떤 선분 S에 수선을 내렸을 때, 그 수선의 발 X를 구함
|
|
68
|
+
* @param point 점 위치벡터 P
|
|
69
|
+
* @param segmentPosition 선분 S의 양 끝점 좌표
|
|
70
|
+
* @returns 수선의 발 X점의 좌표벡터
|
|
71
|
+
*/
|
|
72
|
+
function nearestPointOnSegment(point, segmentPosition) {
|
|
73
|
+
/** 선분 출발점 */
|
|
74
|
+
const segmentStartPoint = segmentPosition[0];
|
|
75
|
+
const segmentEndPoint = segmentPosition[1];
|
|
76
|
+
/** 선분의 방향벡터 */
|
|
77
|
+
const SegmentVector = {
|
|
78
|
+
x: segmentEndPoint.x - segmentStartPoint.x,
|
|
79
|
+
y: segmentEndPoint.y - segmentStartPoint.y,
|
|
80
|
+
};
|
|
81
|
+
/** 점에서 선분 출발점으로의 벡터 */
|
|
82
|
+
const AP = {
|
|
83
|
+
x: point.x - segmentStartPoint.x,
|
|
84
|
+
y: point.y - segmentStartPoint.y,
|
|
85
|
+
};
|
|
86
|
+
/** 점에서 선분 종점으로의 벡터 */
|
|
87
|
+
const BP = {
|
|
88
|
+
x: point.x - segmentEndPoint.x,
|
|
89
|
+
y: point.y - segmentEndPoint.y,
|
|
90
|
+
};
|
|
91
|
+
/** 선분 방향벡터와 점에서 선분 출발점으로의 벡터의 내적 */
|
|
92
|
+
const dot_AB_AP = SegmentVector.x * AP.x + SegmentVector.y * AP.y;
|
|
93
|
+
/** 선분 방향벡터와 점에서 선분 종점으로의 벡터의 내적 */
|
|
94
|
+
const dot_BA_AP = -SegmentVector.x * BP.x - SegmentVector.y * BP.y;
|
|
95
|
+
// dot_AB_AP + dot_BA_AP = |B - A|² 이므로, 0이면 선분의 길이가 0 (점으로 퇴화)
|
|
96
|
+
// 부동소수점 연산 오차를 고려하여 매우 작은 값(epsilon) 이하인 경우도 처리
|
|
97
|
+
const epsilon = 1e-10;
|
|
98
|
+
const denominator = dot_AB_AP + dot_BA_AP;
|
|
99
|
+
if (Math.abs(denominator) < epsilon) {
|
|
100
|
+
// 선분이 너무 짧아 점으로 퇴화된 경우, 시작점을 정답으로 반환
|
|
101
|
+
// 그렇지 않을 경우 const t 를 계산할떄 divide by zero 에러가 발생할 수 있음
|
|
102
|
+
return segmentStartPoint;
|
|
103
|
+
}
|
|
104
|
+
if (dot_AB_AP <= 0)
|
|
105
|
+
return segmentStartPoint;
|
|
106
|
+
else if (dot_BA_AP <= 0)
|
|
107
|
+
return segmentPosition[1];
|
|
108
|
+
else {
|
|
109
|
+
const t = dot_AB_AP / denominator;
|
|
110
|
+
return {
|
|
111
|
+
x: segmentStartPoint.x + SegmentVector.x * t,
|
|
112
|
+
y: segmentStartPoint.y + SegmentVector.y * t,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
geo.nearestPointOnSegment = nearestPointOnSegment;
|
|
117
|
+
/**
|
|
118
|
+
* 주어진 점이 다각형 내부에 있는지 확인
|
|
119
|
+
*/
|
|
120
|
+
function isPointInPolygon(point, polygon) {
|
|
121
|
+
let crosses = 0;
|
|
122
|
+
for (const _i in polygon) {
|
|
123
|
+
const i = Number(_i);
|
|
124
|
+
const j = (i + 1) % polygon.length;
|
|
125
|
+
const p1 = polygon[i];
|
|
126
|
+
const p2 = polygon[j];
|
|
127
|
+
// NOTE: p1.y 와 p2.y 가 같은 값이면 if 조건은 만족할 수가 없다
|
|
128
|
+
// 따라서 if 내에서 p2.y - p1.y 가 0이 되는 경우는 없으므로 divide by zero 에러는 발생할 수 없다
|
|
129
|
+
if (p1.y > point.y !== p2.y > point.y) {
|
|
130
|
+
const atx = ((p2.x - p1.x) * (point.y - p1.y)) / (p2.y - p1.y) + p1.x;
|
|
131
|
+
if (point.x < atx)
|
|
132
|
+
crosses += 1;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return crosses % 2 === 1;
|
|
136
|
+
}
|
|
137
|
+
geo.isPointInPolygon = isPointInPolygon;
|
|
138
|
+
/**
|
|
139
|
+
* 선분을 주어진 길이만큼 양쪽을 깎는다.
|
|
140
|
+
*/
|
|
141
|
+
function trimSegment(params) {
|
|
142
|
+
const { point1, point2, shortenLength1: offsetRadius1 = 0, shortenLength2: offsetRadius2 = 0, } = params;
|
|
143
|
+
const angle = math.vectorSlope({
|
|
144
|
+
point1,
|
|
145
|
+
point2,
|
|
146
|
+
});
|
|
147
|
+
const fromPoint = rotateVectors({
|
|
148
|
+
points: [
|
|
149
|
+
{
|
|
150
|
+
x: point1.x + offsetRadius1,
|
|
151
|
+
y: point1.y,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
rotateAngle: angle,
|
|
155
|
+
centerPosition: { x: point1.x, y: point1.y },
|
|
156
|
+
});
|
|
157
|
+
const destPoint = rotateVectors({
|
|
158
|
+
points: [
|
|
159
|
+
{
|
|
160
|
+
x: point2.x - offsetRadius2,
|
|
161
|
+
y: point2.y,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
rotateAngle: angle,
|
|
165
|
+
centerPosition: { x: point2.x, y: point2.y },
|
|
166
|
+
});
|
|
167
|
+
return { point1: fromPoint[0], point2: destPoint[0] };
|
|
168
|
+
}
|
|
169
|
+
geo.trimSegment = trimSegment;
|
|
170
|
+
/**
|
|
171
|
+
* 실좌표계 상 위치를 화면좌표계로 변환
|
|
172
|
+
* @param pose
|
|
173
|
+
* @param origin 원점을 삼으려는 화면좌표계 상 좌표
|
|
174
|
+
* @param pixelPerMeter 실좌표계의 1미터당 픽셀 수
|
|
175
|
+
*/
|
|
176
|
+
function transformPose(pose, origin, pixelPerMeter) {
|
|
177
|
+
return {
|
|
178
|
+
stagex: origin.x + pose.x * pixelPerMeter,
|
|
179
|
+
stagey: origin.y - pose.y * pixelPerMeter,
|
|
180
|
+
degree: math.degree(pose.theta, { invertY: false }),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
geo.transformPose = transformPose;
|
|
184
|
+
/**
|
|
185
|
+
* 화면좌표계상 위치를 실좌표계로 변환
|
|
186
|
+
* @param position
|
|
187
|
+
* @param realOrigin 원점을 삼으려는 화면좌표계 상 좌표
|
|
188
|
+
* @param pixelPerMeter 실좌표계의 1미터당 픽셀 수
|
|
189
|
+
*/
|
|
190
|
+
function transformPosition(position, realOrigin, pixelPerMeter) {
|
|
191
|
+
if (pixelPerMeter === 0) {
|
|
192
|
+
console.error('transformPosition: pixelPerMeter must be greater than 0');
|
|
193
|
+
return {
|
|
194
|
+
x: 0,
|
|
195
|
+
y: 0,
|
|
196
|
+
theta: 0,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
x: (position.stagex - realOrigin.x) / pixelPerMeter,
|
|
201
|
+
y: ((position.stagey - realOrigin.y) / pixelPerMeter) * -1,
|
|
202
|
+
theta: math.radian(position.degree, { invertY: false }),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
geo.transformPosition = transformPosition;
|
|
206
|
+
/**
|
|
207
|
+
* 화면좌표계에서 viewport 관련 유용한 벡터들 계산
|
|
208
|
+
*/
|
|
209
|
+
function calcViewportVectors(opt) {
|
|
210
|
+
const { viewportSize, referenceVector, cameraZoomScale: scale = 1 } = opt;
|
|
211
|
+
/** 원점에서 viewport center 로 향하는 벡터 */
|
|
212
|
+
const OV = { x: viewportSize.x / 2, y: viewportSize.y / 2 };
|
|
213
|
+
/** 원점에서 참조점으로 향하는 벡터 */
|
|
214
|
+
const OR = { x: referenceVector.x * scale, y: referenceVector.y * scale };
|
|
215
|
+
return {
|
|
216
|
+
/** 참조점이 viewport 중앙으로 가는 벡터 */
|
|
217
|
+
toViewportCenter: {
|
|
218
|
+
x: OV.x - OR.x,
|
|
219
|
+
y: OV.y - OR.y,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
geo.calcViewportVectors = calcViewportVectors;
|
|
224
|
+
/**
|
|
225
|
+
* originPoint 와 endPoint 를 잇는 선분을 가정하고
|
|
226
|
+
* extendLength 만큼 양쪽으로 늘이거나 줄인 선분을 반환
|
|
227
|
+
* (extendLength는 음수일 수도 있음)
|
|
228
|
+
*
|
|
229
|
+
* @returns { origin: BalcanGeo.Vector2, end: BalcanGeo.Vector2 }
|
|
230
|
+
*/
|
|
231
|
+
function extendSegment(params) {
|
|
232
|
+
const { originPoint, endPoint, extendLength } = params;
|
|
233
|
+
const dx = endPoint.x - originPoint.x;
|
|
234
|
+
const dy = endPoint.y - originPoint.y;
|
|
235
|
+
const length = Math.sqrt(dx * dx + dy * dy);
|
|
236
|
+
// 길이가 0인 경우 그대로 반환
|
|
237
|
+
if (length === 0) {
|
|
238
|
+
return { origin: { ...originPoint }, end: { ...endPoint } };
|
|
239
|
+
}
|
|
240
|
+
// 단위벡터 (방향 유지, 음수 extendLength면 짧아짐)
|
|
241
|
+
const ux = dx / length;
|
|
242
|
+
const uy = dy / length;
|
|
243
|
+
// 확장된 점 계산 (extendLength가 음수라면 줄어듦)
|
|
244
|
+
const newOrigin = {
|
|
245
|
+
x: originPoint.x - ux * extendLength,
|
|
246
|
+
y: originPoint.y - uy * extendLength,
|
|
247
|
+
};
|
|
248
|
+
const newEnd = {
|
|
249
|
+
x: endPoint.x + ux * extendLength,
|
|
250
|
+
y: endPoint.y + uy * extendLength,
|
|
251
|
+
};
|
|
252
|
+
return { origin: newOrigin, end: newEnd };
|
|
253
|
+
}
|
|
254
|
+
geo.extendSegment = extendSegment;
|
|
255
|
+
})(geo || (geo = {}));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare namespace math {
|
|
2
|
+
function average(...values: number[]): number;
|
|
3
|
+
function radian(deg: number, opt?: {
|
|
4
|
+
invertY?: boolean;
|
|
5
|
+
toFixed?: number;
|
|
6
|
+
}): number;
|
|
7
|
+
function radian(deg: string, opt?: {
|
|
8
|
+
invertY?: boolean;
|
|
9
|
+
toFixed?: number;
|
|
10
|
+
}): number;
|
|
11
|
+
function degree(rad: number, opt?: {
|
|
12
|
+
invertY?: boolean;
|
|
13
|
+
toFixed?: number;
|
|
14
|
+
}): number;
|
|
15
|
+
function degree(rad: string, opt?: {
|
|
16
|
+
invertY?: boolean;
|
|
17
|
+
toFixed?: number;
|
|
18
|
+
}): number;
|
|
19
|
+
/** 주어진 각도를 -180 ~ 180 범위로 좁힌다 */
|
|
20
|
+
function normalizeAngle(degree: number): number;
|
|
21
|
+
/** 위치 간 거리를 구한다, 위치는 Vector와 Position 타입을 사용할 수 있다 */
|
|
22
|
+
function vectorDistance<T extends BalcanGeo.Vector2 | BalcanGeo.Position>(p1: T, p2: T): number;
|
|
23
|
+
/** 두 점을 잇는 직선의 기울기를 구한다 */
|
|
24
|
+
function vectorSlope<T extends BalcanGeo.Vector2 | BalcanGeo.Position>(params: {
|
|
25
|
+
point1: T;
|
|
26
|
+
point2: T;
|
|
27
|
+
outputUnit?: 'degree' | 'radian';
|
|
28
|
+
}): number;
|
|
29
|
+
/** 벡터 덧셈 */
|
|
30
|
+
function vectorAdd<T extends BalcanGeo.Vector2 | BalcanGeo.Pose>(v1: T, v2: T): T;
|
|
31
|
+
/** 벡터 내적 */
|
|
32
|
+
function vectorDot<T extends BalcanGeo.Vector2 | BalcanGeo.Pose>(v1: T, v2: T): number;
|
|
33
|
+
/** position like 자료형을 vector2d 로 변환함 */
|
|
34
|
+
function vector2d<T extends BalcanGeo.Vector2 | BalcanGeo.Position | BalcanGeo.Pose>(point: T): BalcanGeo.Vector2;
|
|
35
|
+
}
|