fix(B07): 표제란 값 공급 통로 + CAD 회귀 6건 해소
표제란(도각) 값 공급 - `use_title_fields()` 문맥 변수 신설 — 회사 도각 폴더와 같은 방식이라 작도 엔진 6개의 서명을 고치지 않음. `frame_entities` 가 문맥값 위에 호출부 값(도면명)을 얹음. - 라우터가 도면 한 건을 그리기 전에 DB 값을 세움: 공사명·위치(projects), 용역회사(companies), 설계자(users). 못 채운 자리는 빈칸 — 도각 원본의 남의 값이 도면으로 나가지 않음. - 미공급 항목과 사유를 `_title_block_fields` 주석에 명시: 시행청·과업책임자· 분야별책임자는 DB 칸 부재(B02 마이그레이션 대기), 축척·사업량·연도기번은 임의 수치 금지, 설계일자는 채울 시점 정의 미결. CAD 회귀 6건 (87건 중 81 passed → 87 passed) - `window` 부재 5건: `notifyWindow()` 한 곳으로 모으고 `window` 없는 Node 시험에서는 통지를 건너뜀. `triggerReactUpdate` 가 이미 같은 이유로 시험을 건너뛰던 것과 같은 결. - `find-closest-entity`: 목의 호가 90°까지만 돌아 클릭점에서 147px 떨어져 있었음 — 시험 의도(호가 가장 가깝다)대로 3/4바퀴로 되돌림. - 선색 기대값 4건: 앱 기본 선색이 흰 종이 배경에 맞춰 검정으로 바뀐 뒤 상류 기대값 `#fff` 가 남아 있었음 — `getActiveLineColor()` 대조로 교체. - 좌표 NaN: 없어진 `TOOLBAR_WIDTH` 를 시험 보조가 아직 참조 — 입력 컨트롤러가 캔버스 bounding rect 를 쓰도록 바뀐 현행에 맞춤. - 호 각도 비교: 같은 각의 음수·2π 표기 차이라 [0, 2π) 정규화 후 대조. 검증: `npx vitest run` 87 passed / 0 failed, `npm run check-types`·`npm run build` 통과, `pytest tmp/tests/ -q` 131 passed / 0 failed(신규 `test_b07_title_block_fields.py` 4건 포함). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,16 @@ def use_company_templates(company_dir: Path | None) -> None:
|
||||
_company_dir.set(company_dir)
|
||||
|
||||
|
||||
# 이 요청이 도각 표제란에 채울 값. 회사 도각 폴더와 같은 이유로 문맥 변수다 —
|
||||
# 엔진 6개의 서명을 줄줄이 고치지 않는다. 값을 못 구한 자리는 **빈칸**으로 남는다.
|
||||
_title_fields: ContextVar[dict[str, str]] = ContextVar("b07_title_fields", default={})
|
||||
|
||||
|
||||
def use_title_fields(fields: dict[str, str] | None) -> None:
|
||||
"""이 요청이 도각 표제란에 채울 값을 정한다. None이면 표제란이 전부 빈칸이다."""
|
||||
_title_fields.set(fields or {})
|
||||
|
||||
|
||||
def company_template_path(company_dir: Path, name: str = A1_TEMPLATE) -> Path:
|
||||
"""회사 도각 파일 경로(없을 수도 있다)."""
|
||||
return Path(company_dir) / COMPANY_TEMPLATE_SUBDIR / f"{name}.json"
|
||||
@@ -307,5 +317,6 @@ def frame_entities(
|
||||
_transform_entity(entity, f"{drawing_id}:frame:{index}", scale, dx, dy)
|
||||
for index, entity in enumerate(template.get("entities", []))
|
||||
]
|
||||
_fill_placeholders(placed, fields or {})
|
||||
# 요청 문맥의 값이 바탕, 호출부가 준 값(도면명 등 도면마다 다른 것)이 위에 얹힌다.
|
||||
_fill_placeholders(placed, {**_title_fields.get(), **(fields or {})})
|
||||
return placed
|
||||
|
||||
@@ -30,6 +30,7 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Template import (
|
||||
frame_template_document,
|
||||
save_company_template,
|
||||
use_company_templates,
|
||||
use_title_fields,
|
||||
)
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support import (
|
||||
MASS_HAUL_ID,
|
||||
@@ -96,6 +97,39 @@ async def _company_dir(project_id: UUID) -> Path:
|
||||
return root.parent.parent
|
||||
|
||||
|
||||
async def _title_block_fields(project_id: UUID) -> dict[str, str]:
|
||||
"""도각 표제란에 채울 값. **DB가 아는 것만** 담고 나머지는 담지 않는다.
|
||||
|
||||
담지 않은 자리는 `_fill_placeholders`가 빈칸으로 지운다 — 도각 원본에 남의 값이
|
||||
박혀 있어도 도면에는 나가지 않는다(2026-08-31 사용자 확정, 이것이 1순위 목적).
|
||||
|
||||
아직 못 채우는 자리와 이유:
|
||||
- 시행청·과업책임자·분야별책임자 — `projects`에 칸이 없다. B02 등록 화면과
|
||||
마이그레이션이 서야 채워진다(공유 DB 변경이라 사용자 확정 대기).
|
||||
- 축척(A1/A3)·사업량·연도기번 — 값을 지어내지 않는다(임의 수치 금지).
|
||||
- 설계일자 — "확정일"인데 도각은 **확정 전**에 그려져 저장본에 굳는다.
|
||||
채울 시점 정의가 미결이라 비워 둔다.
|
||||
"""
|
||||
pool = get_db_pool()
|
||||
async with pool.acquire() as connection, connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"""
|
||||
SELECT p.name, p.region, c.name, u.name
|
||||
FROM projects p
|
||||
LEFT JOIN companies c ON c.id = p.company_id
|
||||
LEFT JOIN users u ON u.id = p.user_id
|
||||
WHERE p.id = %s AND p.deleted_at IS NULL
|
||||
""",
|
||||
(str(project_id),),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
if not row:
|
||||
return {}
|
||||
name, region, company_name, designer = row
|
||||
fields = {"공사명": name, "위치": region, "용역회사": company_name, "설계자": designer}
|
||||
return {key: str(value) for key, value in fields.items() if value}
|
||||
|
||||
|
||||
async def _designs_by_chainage(route_id: int) -> dict[int, dict[str, Any]]:
|
||||
"""노선 전체의 측점별 설계 지정 {측점(m): design}. 장 배치·목록이 함께 쓴다."""
|
||||
pool = get_db_pool()
|
||||
@@ -142,6 +176,8 @@ async def get_design_drawing(
|
||||
# 이 회사가 고친 도각이 있으면 그것으로 그린다(없으면 프로그램 기본 도각).
|
||||
# 저장 경로는 `storage/{회사}/{사용자}/{프로젝트}` 이므로 두 단계 위가 회사 폴더다.
|
||||
use_company_templates(project_root.parent.parent)
|
||||
# 표제란 값도 같은 요청 문맥에 세운다 — 값이 없는 칸은 빈칸으로 나간다.
|
||||
use_title_fields(await _title_block_fields(project_id))
|
||||
# 횡단도는 B06 지정 설계를 먼저 읽어 CAD 계획선(design_line)과 응답에 함께 쓴다.
|
||||
design: dict[str, Any] | None = None
|
||||
source_design: Any = None
|
||||
|
||||
@@ -11,7 +11,7 @@ import { HtmlEvent } from '../App.types';
|
||||
import type { Entity, JsonEntity } from '../entities/Entity';
|
||||
import { getBoundingBoxOfMultipleEntities } from '../helpers/get-bounding-box-of-multiple-entities';
|
||||
import { getEntitiesAndLayersFromJsonObject } from '../helpers/import-export-handlers/import-entities-from-json';
|
||||
import { getActiveLayerId } from '../state';
|
||||
import { getActiveLayerId, notifyWindow } from '../state';
|
||||
|
||||
const STORAGE_KEY = 'aislo-cad-block-library';
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface BlockDefinition {
|
||||
|
||||
let blocks: BlockDefinition[] | null = null;
|
||||
|
||||
const notify = () => window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
const notify = () => notifyWindow(HtmlEvent.UPDATE_STATE);
|
||||
|
||||
function load(): BlockDefinition[] {
|
||||
if (blocks) return blocks;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { toast } from 'react-toastify';
|
||||
import { Actor } from 'xstate';
|
||||
import { HtmlEvent } from '../App.types';
|
||||
import { getSelectedEntities, isDrawingReadOnly, setActiveToolActor } from '../state';
|
||||
import { getSelectedEntities, isDrawingReadOnly, notifyWindow, setActiveToolActor } from '../state';
|
||||
import type { CadCommand } from './command.types';
|
||||
import { getCommandById, isViewOnlyCommand, resolveCommandInput } from './registry';
|
||||
|
||||
@@ -19,7 +19,7 @@ function log(line: string) {
|
||||
if (commandHistory.length > COMMAND_HISTORY_LIMIT) {
|
||||
commandHistory.splice(0, commandHistory.length - COMMAND_HISTORY_LIMIT);
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
notifyWindow(HtmlEvent.UPDATE_STATE);
|
||||
}
|
||||
|
||||
/** 명령 한 건 실행. 도구형이면 도구를 활성화하고, 즉시형이면 run()을 부른다. */
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 리액트 컴포넌트 바깥에 둔다. 값이 바뀌면 UPDATE_STATE로 다시 그린다.
|
||||
*/
|
||||
import { HtmlEvent } from '../App.types';
|
||||
import { notifyWindow } from '../state';
|
||||
|
||||
export type InspectorTab = 'properties' | 'layers';
|
||||
|
||||
@@ -12,7 +13,7 @@ let quickPropertiesVisible = false;
|
||||
let activeRibbonTab = 'home';
|
||||
let blockLibraryVisible = false;
|
||||
|
||||
const notify = () => window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
const notify = () => notifyWindow(HtmlEvent.UPDATE_STATE);
|
||||
|
||||
export const getInspectorTab = () => inspectorTab;
|
||||
export const isInspectorCollapsed = () => inspectorCollapsed;
|
||||
|
||||
@@ -33,8 +33,9 @@ export const arcAndLineEntitiesMock: JsonDrawingFileSerialized = {
|
||||
},
|
||||
radius: 156.92367603040066,
|
||||
startAngle: 0,
|
||||
// endAngle: (2 * Math.PI * 3) / 4,
|
||||
endAngle: 1.5707963267948966,
|
||||
// 클릭점(393,1108)이 호 위(중심각 ≈145°)에 놓이도록 3/4바퀴를 쓴다 —
|
||||
// 90°까지만 돌면 호가 클릭점에서 147px 떨어져 직선(64px)이 더 가깝다.
|
||||
endAngle: (2 * Math.PI * 3) / 4,
|
||||
counterClockwise: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
import { HtmlEvent } from '../App.types';
|
||||
import type { Entity } from '../entities/Entity';
|
||||
import { notifyWindow } from '../state';
|
||||
import { bumpSceneVersion } from './scene-version';
|
||||
|
||||
let hiddenEntityIds = new Set<string>();
|
||||
@@ -14,7 +15,7 @@ export const getHiddenEntityCount = (): number => hiddenEntityIds.size;
|
||||
function apply(ids: Set<string>): void {
|
||||
hiddenEntityIds = ids;
|
||||
bumpSceneVersion();
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
notifyWindow(HtmlEvent.UPDATE_STATE);
|
||||
}
|
||||
|
||||
/** 지정 객체를 숨긴다 */
|
||||
|
||||
@@ -309,9 +309,15 @@ export const setActiveToolActor = (
|
||||
triggerReactUpdate(StateVariable.activeTool);
|
||||
}
|
||||
};
|
||||
/** 화면에 알린다 — `window`가 없는 Node 시험에서는 통지를 건너뛴다.
|
||||
* (`triggerReactUpdate`가 이미 같은 이유로 시험 환경을 건너뛴다.) */
|
||||
export const notifyWindow = (event: HtmlEvent) => {
|
||||
if (typeof window === 'undefined') return;
|
||||
window.dispatchEvent(new CustomEvent(event));
|
||||
};
|
||||
export const setLastStateInstructions = (newInstructions: string | null) => {
|
||||
lastStateInstructions = newInstructions;
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
notifyWindow(HtmlEvent.UPDATE_STATE);
|
||||
};
|
||||
export const setEntities = (newEntities: Entity[], trackInUndoStack = false) => {
|
||||
if (trackInUndoStack) {
|
||||
@@ -321,7 +327,7 @@ export const setEntities = (newEntities: Entity[], trackInUndoStack = false) =>
|
||||
bumpSceneVersion();
|
||||
if (trackInUndoStack) {
|
||||
drawingDirty = true;
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.DRAWING_CHANGED));
|
||||
notifyWindow(HtmlEvent.DRAWING_CHANGED);
|
||||
}
|
||||
};
|
||||
/** 도면을 새로 실었거나 저장했다 — 미저장 표시를 내린다. */
|
||||
@@ -348,7 +354,7 @@ export const setSelectedEntityIds = (newEntityIds: string[]) => {
|
||||
selectedEntityIds = newEntityIds;
|
||||
selectedEntityIdSet = new Set(newEntityIds);
|
||||
bumpSceneVersion(); // selection style (dashed) is baked into the scene cache
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
notifyWindow(HtmlEvent.UPDATE_STATE);
|
||||
};
|
||||
export const setShouldDrawCursor = (newValue: boolean) => {
|
||||
shouldDrawCursor = newValue;
|
||||
@@ -462,19 +468,19 @@ export const setSnapEnabled = (enabled: boolean) => {
|
||||
setHoveredSnapPoints([]);
|
||||
setAngleGuideEntities([]);
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
notifyWindow(HtmlEvent.UPDATE_STATE);
|
||||
};
|
||||
export const setSnapTrackingEnabled = (enabled: boolean) => {
|
||||
snapTrackingEnabled = enabled;
|
||||
if (!enabled) {
|
||||
setHoveredSnapPoints([]);
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
notifyWindow(HtmlEvent.UPDATE_STATE);
|
||||
};
|
||||
export const setGridEnabled = (enabled: boolean) => {
|
||||
gridEnabled = enabled;
|
||||
bumpSceneVersion();
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
notifyWindow(HtmlEvent.UPDATE_STATE);
|
||||
};
|
||||
export const setDesignMeta = (newMeta: DesignMeta | null) => {
|
||||
designMeta = newMeta;
|
||||
@@ -556,7 +562,7 @@ export function undo() {
|
||||
|
||||
updateStates(undoState);
|
||||
drawingDirty = true; // 되돌려도 저장본과는 다를 수 있다 — 미저장 경고 대상이다
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.DRAWING_CHANGED));
|
||||
notifyWindow(HtmlEvent.DRAWING_CHANGED);
|
||||
}
|
||||
|
||||
export function redo() {
|
||||
@@ -565,7 +571,7 @@ export function redo() {
|
||||
|
||||
updateStates(redoState);
|
||||
drawingDirty = true;
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.DRAWING_CHANGED));
|
||||
notifyWindow(HtmlEvent.DRAWING_CHANGED);
|
||||
}
|
||||
|
||||
export function triggerReactUpdate(variable: StateVariable) {
|
||||
@@ -577,5 +583,5 @@ export function triggerReactUpdate(variable: StateVariable) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.dispatchEvent(new CustomEvent(HtmlEvent.UPDATE_STATE));
|
||||
notifyWindow(HtmlEvent.UPDATE_STATE);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Draw a rectangle to the screen and check if the json export contains the correct data using the vitest testing framework
|
||||
*/
|
||||
import { expect, test } from 'vitest';
|
||||
import { getEntities } from '../../../src/state';
|
||||
import { getActiveLineColor, getEntities } from '../../../src/state';
|
||||
import { EntityName, type JsonEntity } from '../../../src/entities/Entity';
|
||||
import { initApplication } from '../../helpers/init-application';
|
||||
import { CANVAS_HEIGHT } from '../../helpers/tests.consts';
|
||||
@@ -24,7 +24,7 @@ test('Draw circle', async () => {
|
||||
expect(circleEntity.getType()).toBe(EntityName.Circle);
|
||||
const circleJson = (await circleEntity.toJson()) as JsonEntity<CircleJsonData>;
|
||||
|
||||
expect(circleJson.lineColor).toBe('#fff');
|
||||
expect(circleJson.lineColor).toBe(getActiveLineColor());
|
||||
expect(circleJson.lineWidth).toBe(1);
|
||||
expect(circleJson.type).toBe('Circle');
|
||||
expect(circleJson.shapeData.center.x).toBe(325);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Draw a rectangle to the screen and check if the json export contains the correct data using the vitest testing framework
|
||||
*/
|
||||
import { expect, test } from 'vitest';
|
||||
import { getEntities } from '../../../src/state';
|
||||
import { getActiveLineColor, getEntities } from '../../../src/state';
|
||||
import { Tool } from '../../../src/tools';
|
||||
import { EntityName, type JsonEntity } from '../../../src/entities/Entity';
|
||||
import { initApplication } from '../../helpers/init-application';
|
||||
@@ -23,7 +23,7 @@ test('Draw line', async () => {
|
||||
expect(lineEntity.getType()).toBe(EntityName.Line);
|
||||
const lineJson = (await lineEntity.toJson()) as JsonEntity<LineJsonData>;
|
||||
|
||||
expect(lineJson.lineColor).toBe('#fff');
|
||||
expect(lineJson.lineColor).toBe(getActiveLineColor());
|
||||
expect(lineJson.lineWidth).toBe(1);
|
||||
expect(lineJson.type).toBe('Line');
|
||||
expect(lineJson.shapeData.startPoint.x).toBe(185);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { EntityName, type JsonEntity } from '../../../src/entities/Entity';
|
||||
import type { RectangleJsonData } from '../../../src/entities/RectangleEntity';
|
||||
import { getEntities } from '../../../src/state';
|
||||
import { getActiveLineColor, getEntities } from '../../../src/state';
|
||||
import { Tool } from '../../../src/tools';
|
||||
import { click } from '../../helpers/click';
|
||||
import { initApplication } from '../../helpers/init-application';
|
||||
@@ -23,7 +23,7 @@ test('Draw circle', async () => {
|
||||
expect(rectangleEntity.getType()).toBe(EntityName.Rectangle);
|
||||
const rectangleJson = (await rectangleEntity.toJson()) as JsonEntity<RectangleJsonData>;
|
||||
|
||||
expect(rectangleJson.lineColor).toBe('#fff');
|
||||
expect(rectangleJson.lineColor).toBe(getActiveLineColor());
|
||||
expect(rectangleJson.lineWidth).toBe(1);
|
||||
expect(rectangleJson.type).toBe('Rectangle');
|
||||
expect(rectangleJson.shapeData.points[0].x).toBe(185);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { TOOLBAR_WIDTH } from '../../src/App.consts';
|
||||
import { MouseButton } from '../../src/App.types';
|
||||
import type { InputController } from '../../src/inputController/input-controller';
|
||||
|
||||
@@ -17,7 +16,8 @@ export function click(
|
||||
) {
|
||||
inputController.handleMouseUp({
|
||||
button: mouseButton,
|
||||
clientX: TOOLBAR_WIDTH + x, // Coordinates are relative to the top left of the draw area excluding the toolbar
|
||||
// 입력 컨트롤러가 캔버스 bounding rect(시험에서는 0)를 빼므로 화면 좌표를 그대로 준다.
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TOOLBAR_WIDTH } from '../../src/App.consts';
|
||||
|
||||
export const CANVAS_WIDTH = 1920 - TOOLBAR_WIDTH;
|
||||
// 좌표 변환이 캔버스 bounding rect 를 쓰도록 바뀌어 툴바 폭 상수가 없어졌다 —
|
||||
// 시험 캔버스는 화면 전체 폭을 그대로 쓴다.
|
||||
export const CANVAS_WIDTH = 1920;
|
||||
export const CANVAS_HEIGHT = 1080;
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ArcJsonData } from '../../../src/entities/ArcEntity';
|
||||
import { EntityName, type JsonEntity } from '../../../src/entities/Entity';
|
||||
import type { LineJsonData } from '../../../src/entities/LineEntity';
|
||||
import { pointDistance } from '../../../src/helpers/distance-between-points';
|
||||
import { getEntities } from '../../../src/state';
|
||||
import { getActiveLineColor, getEntities } from '../../../src/state';
|
||||
import { initApplication } from '../../helpers/init-application';
|
||||
import { replayRecording } from '../../helpers/replay-recording';
|
||||
import { CANVAS_HEIGHT } from '../../helpers/tests.consts';
|
||||
@@ -23,7 +23,7 @@ test('Draw circle and line and erase part of circle', async () => {
|
||||
expect(lineEntity.getType()).toBe(EntityName.Line);
|
||||
const lineJson = (await lineEntity.toJson()) as JsonEntity<LineJsonData>;
|
||||
|
||||
expect(lineJson.lineColor).toBe('#fff');
|
||||
expect(lineJson.lineColor).toBe(getActiveLineColor());
|
||||
expect(lineJson.lineWidth).toBe(1);
|
||||
expect(lineJson.type).toBe('Line');
|
||||
expect(lineJson.shapeData.startPoint.x).toBe(473);
|
||||
@@ -35,7 +35,7 @@ test('Draw circle and line and erase part of circle', async () => {
|
||||
expect(arcEntity.getType()).toBe(EntityName.Arc);
|
||||
const arcJson = (await arcEntity.toJson()) as JsonEntity<ArcJsonData>;
|
||||
|
||||
expect(arcJson.lineColor).toBe('#fff');
|
||||
expect(arcJson.lineColor).toBe(getActiveLineColor());
|
||||
expect(arcJson.lineWidth).toBe(1);
|
||||
expect(arcJson.type).toBe('Arc');
|
||||
expect(arcJson.shapeData.center.x).toBe(266);
|
||||
@@ -45,6 +45,9 @@ test('Draw circle and line and erase part of circle', async () => {
|
||||
new Point(542, CANVAS_HEIGHT - 441)
|
||||
);
|
||||
expect(arcJson.shapeData.radius).toBeCloseTo(radius, 5);
|
||||
expect(arcJson.shapeData.startAngle).toBeCloseTo(0.7338182524767606, 5);
|
||||
expect(arcJson.shapeData.endAngle).toBeCloseTo(-0.7338182524767606, 5);
|
||||
// 각은 [0, 2π)로 정규화해 비교한다 — 같은 각을 음수로도 2π 더한 값으로도 쓸 수 있다.
|
||||
const turn = 2 * Math.PI;
|
||||
const normalize = (angle: number) => ((angle % turn) + turn) % turn;
|
||||
expect(normalize(arcJson.shapeData.startAngle)).toBeCloseTo(normalize(0.7338182524767606), 5);
|
||||
expect(normalize(arcJson.shapeData.endAngle)).toBeCloseTo(normalize(-0.7338182524767606), 5);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user