This commit is contained in:
2026-07-19 19:18:14 +09:00
parent ef0dab9bc3
commit 6ec8fca60e
23 changed files with 1130 additions and 790 deletions
+25 -2
View File
@@ -33,6 +33,30 @@ def _load_route_polyline(project_root: Path, route_data_path: str) -> list[list[
return [[float(c[0]), float(c[1]), float(c[2]) if len(c) > 2 else 0.0] for c in coords] return [[float(c[0]), float(c[1]), float(c[2]) if len(c) > 2 else 0.0] for c in coords]
def cross_filename(chainage_m: float) -> str:
"""측점 chainage에 대응하는 횡단면 파일명(단일 규칙)."""
return f"cross_{int(round(float(chainage_m))):05d}m.json"
def prune_stale_cross_files(cross_dir: Path, stations: list[Any]) -> set[str]:
"""stations에 없는 잔재 cross_*.json을 삭제하고 유효 파일명 집합을 반환한다.
측점 정보가 비어 있으면 오삭제를 피하기 위해 아무것도 지우지 않고
빈 집합을 반환한다(호출부는 빈 집합이면 필터를 생략한다).
"""
valid = {
cross_filename(station["chainage_m"])
for station in stations
if isinstance(station, dict) and isinstance(station.get("chainage_m"), (int, float))
}
if not valid:
return valid
for path in cross_dir.glob("cross_*.json"):
if path.name not in valid:
path.unlink(missing_ok=True)
return valid
def _cross_summary(cross_section: dict[str, Any]) -> dict[str, Any]: def _cross_summary(cross_section: dict[str, Any]) -> dict[str, Any]:
"""횡단면 상세에서 DB data 컬럼에 저장할 요약을 만든다.""" """횡단면 상세에서 DB data 컬럼에 저장할 요약을 만든다."""
samples = cross_section.get("samples", []) samples = cross_section.get("samples", [])
@@ -101,8 +125,7 @@ def run_section_generation(
cross_records: list[dict[str, Any]] = [] cross_records: list[dict[str, Any]] = []
for seq, cross_section in enumerate(result["cross_sections"]): for seq, cross_section in enumerate(result["cross_sections"]):
chainage = float(cross_section["chainage_m"]) chainage = float(cross_section["chainage_m"])
filename = f"cross_{int(round(chainage)):05d}m.json" cross_file = cross_dir / cross_filename(chainage)
cross_file = cross_dir / filename
atomic_write_json(cross_file, cross_section) atomic_write_json(cross_file, cross_section)
cross_records.append( cross_records.append(
{ {
@@ -12,7 +12,10 @@ from fastapi import APIRouter
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
from B05_wf2_Route.B05_wf2_Route_Engine_Sections import run_section_generation from B05_wf2_Route.B05_wf2_Route_Engine_Sections import (
prune_stale_cross_files,
run_section_generation,
)
from B05_wf2_Route.B05_wf2_Route_Engine_Sections_Core import SectionGenerationOptions from B05_wf2_Route.B05_wf2_Route_Engine_Sections_Core import SectionGenerationOptions
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Repository import ( from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Repository import (
confirm_sections_for_route, confirm_sections_for_route,
@@ -122,9 +125,12 @@ def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dic
raise FileNotFoundError("횡단면 상세 파일을 찾을 수 없습니다.") raise FileNotFoundError("횡단면 상세 파일을 찾을 수 없습니다.")
longitudinal = json.loads(longitudinal_path.read_text(encoding="utf-8")) longitudinal = json.loads(longitudinal_path.read_text(encoding="utf-8"))
stations = longitudinal.get("stations") if isinstance(longitudinal, dict) else None
valid_names = prune_stale_cross_files(cross_dir, stations if isinstance(stations, list) else [])
cross_sections = [ cross_sections = [
json.loads(path.read_text(encoding="utf-8")) json.loads(path.read_text(encoding="utf-8"))
for path in sorted(cross_dir.glob("cross_*.json")) for path in sorted(cross_dir.glob("cross_*.json"))
if not valid_names or path.name in valid_names
] ]
if not isinstance(longitudinal, dict) or not all( if not isinstance(longitudinal, dict) or not all(
isinstance(section, dict) for section in cross_sections isinstance(section, dict) for section in cross_sections
@@ -12,6 +12,7 @@ from fastapi import APIRouter
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
from B05_wf2_Route.B05_wf2_Route_Engine_Sections import prune_stale_cross_files
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Repository import ( from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Repository import (
get_confirmed_route_context, get_confirmed_route_context,
get_longitudinal_section, get_longitudinal_section,
@@ -63,11 +64,16 @@ def _read_json(path: Path) -> dict[str, Any]:
return payload return payload
def _cross_files(longitudinal_path: Path) -> list[Path]: def _cross_files(longitudinal_path: Path, longitudinal: dict[str, Any]) -> list[Path]:
cross_dir = longitudinal_path.parent.parent / "cross_sections" cross_dir = longitudinal_path.parent.parent / "cross_sections"
if not cross_dir.is_dir(): if not cross_dir.is_dir():
raise FileNotFoundError("B06 횡단면 파일을 찾을 수 없습니다.") raise FileNotFoundError("B06 횡단면 파일을 찾을 수 없습니다.")
return sorted(cross_dir.glob("cross_*.json")) stations = longitudinal.get("stations")
valid_names = prune_stale_cross_files(cross_dir, stations if isinstance(stations, list) else [])
files = sorted(cross_dir.glob("cross_*.json"))
if valid_names:
files = [path for path in files if path.name in valid_names]
return files
def _station_map(longitudinal: dict[str, Any]) -> dict[int, dict[str, Any]]: def _station_map(longitudinal: dict[str, Any]) -> dict[int, dict[str, Any]]:
@@ -114,7 +120,7 @@ def _drawing_list(project_root: Path, longitudinal_path: Path) -> list[DesignDra
confirmed=bool(manifest_drawings.get("longitudinal", {}).get("confirmed")), confirmed=bool(manifest_drawings.get("longitudinal", {}).get("confirmed")),
) )
] ]
for path in _cross_files(longitudinal_path): for path in _cross_files(longitudinal_path, longitudinal):
match = _CROSS_ID.fullmatch(path.stem) match = _CROSS_ID.fullmatch(path.stem)
if not match: if not match:
continue continue
@@ -80,18 +80,21 @@ export const MAX_MARKED_SNAP_POINTS = 3;
/** /**
* Length of the extensions that extend past the measurement arrows of a measurement * Length of the extensions that extend past the measurement arrows of a measurement
* Screen pixels: converted to world units per current zoom so drawings in meters stay legible
*/ */
export const MEASUREMENT_EXTENSION_LENGTH = 20; export const MEASUREMENT_EXTENSION_LENGTH = 12;
/** /**
* Distance that measurement lines stay away from the point of origin of the measurement * Distance that measurement lines stay away from the point of origin of the measurement
* Screen pixels (zoom-independent)
*/ */
export const MEASUREMENT_ORIGIN_MARGIN = 20; export const MEASUREMENT_ORIGIN_MARGIN = 8;
/** /**
* Distance the measurement is drawn while drawing the start and endpoints of the measurements but before the user decides the offset point * Distance the measurement is drawn while drawing the start and endpoints of the measurements but before the user decides the offset point
* Screen pixels (zoom-independent)
*/ */
export const MEASUREMENT_DEFAULT_OFFSET = 200; export const MEASUREMENT_DEFAULT_OFFSET = 60;
/** /**
* Length of the arrow heads for measurements * Length of the arrow heads for measurements
@@ -110,13 +113,15 @@ export const MEASUREMENT_DECIMAL_PLACES = 2;
/** /**
* Distance between the measurement line and the label of the measurement * Distance between the measurement line and the label of the measurement
* Screen pixels (zoom-independent)
*/ */
export const MEASUREMENT_LABEL_OFFSET = 20; export const MEASUREMENT_LABEL_OFFSET = 8;
/** /**
* Size of the measurement labels containing the length of the measurements * Size of the measurement labels containing the length of the measurements
* Screen pixels (zoom-independent)
*/ */
export const MEASUREMENT_FONT_SIZE = 40; export const MEASUREMENT_FONT_SIZE = 16;
/** /**
* Colors for the selection rectangle * Colors for the selection rectangle
@@ -148,6 +148,45 @@ body > canvas[data-id="canvas"] {
font-size: 10px; font-size: 10px;
text-align: center; text-align: center;
} }
.cad-ribbon-props {
gap: 8px;
align-items: center;
}
.cad-prop {
display: flex;
flex-direction: column;
gap: 3px;
align-items: stretch;
font-size: 10px;
color: #8293a5;
}
.cad-prop > span {
text-align: center;
}
.cad-prop select,
.cad-prop input[type='number'] {
height: 24px;
min-width: 64px;
padding: 0 4px;
color: #dce6f2;
background: #2a3745;
border: 1px solid #40505f;
border-radius: 4px;
font-size: 11px;
}
.cad-prop input[type='number'] {
min-width: 48px;
width: 48px;
}
.cad-prop input[type='color'] {
height: 24px;
width: 40px;
padding: 1px;
background: #2a3745;
border: 1px solid #40505f;
border-radius: 4px;
cursor: pointer;
}
.cad-tool { .cad-tool {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -4,10 +4,18 @@ import { Actor } from 'xstate';
import { HtmlEvent, type Layer } from '../App.types'; import { HtmlEvent, type Layer } from '../App.types';
import { exportEntitiesToJsonFile } from '../helpers/import-export-handlers/export-entities-to-json'; import { exportEntitiesToJsonFile } from '../helpers/import-export-handlers/export-entities-to-json';
import { exportEntitiesToLocalStorage } from '../helpers/import-export-handlers/export-entities-to-local-storage'; import { exportEntitiesToLocalStorage } from '../helpers/import-export-handlers/export-entities-to-local-storage';
import type { Entity } from '../entities/Entity';
import { EntityName } from '../entities/Entity';
import { TextEntity } from '../entities/TextEntity';
import { import {
getActiveLayerId, getActiveLayerId,
getActiveLineColor,
getActiveLineDash,
getActiveLineWidth,
getActiveTextStyle,
getActiveToolActor, getActiveToolActor,
getAngleStep, getAngleStep,
getEntities,
getGridEnabled, getGridEnabled,
getInputController, getInputController,
getLastStateInstructions, getLastStateInstructions,
@@ -17,8 +25,13 @@ import {
getSnapEnabled, getSnapEnabled,
redo, redo,
setActiveLayerId, setActiveLayerId,
setActiveLineColor,
setActiveLineDash,
setActiveLineWidth,
setActiveTextStyle,
setActiveToolActor, setActiveToolActor,
setAngleStep, setAngleStep,
setEntities,
setGridEnabled, setGridEnabled,
setLayers, setLayers,
setSnapEnabled, setSnapEnabled,
@@ -69,6 +82,20 @@ const RIBBON_GROUPS: { label: string; tools: RibbonTool[] }[] = [
const COMMANDS = Object.values(Tool); const COMMANDS = Object.values(Tool);
const LINE_TYPES: { value: string; label: string; dash: number[] | undefined }[] = [
{ value: 'solid', label: '실선', dash: undefined },
{ value: 'dashed', label: '파선', dash: [10, 5] },
{ value: 'dashdot', label: '1점쇄선', dash: [12, 4, 2, 4] },
{ value: 'dotted', label: '점선', dash: [2, 4] },
];
const LINE_WIDTHS = [1, 2, 3, 4, 5];
const FONT_FAMILIES = ['Noto Sans KR', 'Malgun Gothic', 'Pretendard', 'Arial', 'monospace'];
const dashToLineType = (dash: number[] | undefined): string =>
LINE_TYPES.find((type) => JSON.stringify(type.dash) === JSON.stringify(dash))?.value ?? 'solid';
export const Toolbar: FC = () => { export const Toolbar: FC = () => {
const [activeTool, setActiveTool] = useState<Tool>(Tool.LINE); const [activeTool, setActiveTool] = useState<Tool>(Tool.LINE);
const [zoom, setZoom] = useState(1); const [zoom, setZoom] = useState(1);
@@ -84,6 +111,10 @@ export const Toolbar: FC = () => {
const [ortho, setOrtho] = useState(getAngleStep() === 90); const [ortho, setOrtho] = useState(getAngleStep() === 90);
const [command, setCommand] = useState(''); const [command, setCommand] = useState('');
const [commandLog, setCommandLog] = useState('준비'); const [commandLog, setCommandLog] = useState('준비');
const [lineColor, setLineColorLocal] = useState(getActiveLineColor());
const [lineWidth, setLineWidthLocal] = useState(getActiveLineWidth());
const [lineType, setLineTypeLocal] = useState(dashToLineType(getActiveLineDash()));
const [textStyle, setTextStyleLocal] = useState(getActiveTextStyle());
const refresh = useCallback(() => { const refresh = useCallback(() => {
setActiveTool(getActiveToolActor()?.getSnapshot()?.context.type ?? Tool.LINE); setActiveTool(getActiveToolActor()?.getSnapshot()?.context.type ?? Tool.LINE);
@@ -99,6 +130,10 @@ export const Toolbar: FC = () => {
setSnap(getSnapEnabled()); setSnap(getSnapEnabled());
setGrid(getGridEnabled()); setGrid(getGridEnabled());
setOrtho(getAngleStep() === 90); setOrtho(getAngleStep() === 90);
setLineColorLocal(getActiveLineColor());
setLineWidthLocal(getActiveLineWidth());
setLineTypeLocal(dashToLineType(getActiveLineDash()));
setTextStyleLocal({ ...getActiveTextStyle() });
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -136,6 +171,55 @@ export const Toolbar: FC = () => {
setZoom(controller.getScreenScale()); setZoom(controller.getScreenScale());
}; };
/** 선택 객체가 있으면 스타일을 즉시 적용하고, 없으면 이후 그리기 기본값만 바꾼다. */
const applyToSelection = useCallback((mutate: (entity: Entity) => void): boolean => {
const selected = getSelectedEntities();
if (!selected.length) return false;
for (const entity of selected) {
mutate(entity);
}
setEntities([...getEntities()], true);
return true;
}, []);
const handleLineColor = (color: string) => {
setActiveLineColor(color);
setLineColorLocal(color);
if (applyToSelection((entity) => (entity.lineColor = color))) {
setCommandLog('선택 객체 색상 변경');
}
};
const handleLineWidth = (width: number) => {
setActiveLineWidth(width);
setLineWidthLocal(width);
if (applyToSelection((entity) => (entity.lineWidth = width))) {
setCommandLog('선택 객체 선굵기 변경');
}
};
const handleLineType = (value: string) => {
const dash = LINE_TYPES.find((type) => type.value === value)?.dash;
setActiveLineDash(dash ? [...dash] : undefined);
setLineTypeLocal(value);
if (applyToSelection((entity) => (entity.lineDash = dash ? [...dash] : undefined))) {
setCommandLog('선택 객체 선종류 변경');
}
};
const handleTextStyle = (patch: Partial<typeof textStyle>) => {
setActiveTextStyle(patch);
setTextStyleLocal((previous) => ({ ...previous, ...patch }));
const applied = applyToSelection((entity) => {
if (entity.getType() === EntityName.Text) {
(entity as TextEntity).setTextOptions(patch);
}
});
if (applied) {
setCommandLog('선택 문자 스타일 변경');
}
};
return ( return (
<> <>
<header className="cad-titlebar controls"> <header className="cad-titlebar controls">
@@ -195,6 +279,83 @@ export const Toolbar: FC = () => {
<span className="cad-ribbon-group__label">{group.label}</span> <span className="cad-ribbon-group__label">{group.label}</span>
</section> </section>
))} ))}
<section className="cad-ribbon-group">
<div className="cad-ribbon-tools cad-ribbon-props">
<label className="cad-prop" title="선 색상">
<span>색상</span>
<input
type="color"
value={lineColor}
onChange={(event) => handleLineColor(event.target.value)}
/>
</label>
<label className="cad-prop" title="선 굵기">
<span>굵기</span>
<select
value={lineWidth}
onChange={(event) => handleLineWidth(Number(event.target.value))}
>
{LINE_WIDTHS.map((width) => (
<option key={width} value={width}>
{width}px
</option>
))}
</select>
</label>
<label className="cad-prop" title="선 종류">
<span>선종류</span>
<select value={lineType} onChange={(event) => handleLineType(event.target.value)}>
{LINE_TYPES.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</label>
</div>
<span className="cad-ribbon-group__label">특성</span>
</section>
<section className="cad-ribbon-group">
<div className="cad-ribbon-tools cad-ribbon-props">
<label className="cad-prop" title="폰트">
<span>폰트</span>
<select
value={textStyle.fontFamily}
onChange={(event) => handleTextStyle({ fontFamily: event.target.value })}
>
{FONT_FAMILIES.map((family) => (
<option key={family} value={family}>
{family}
</option>
))}
</select>
</label>
<label className="cad-prop" title="문자 크기">
<span>크기</span>
<input
type="number"
min={4}
max={120}
value={textStyle.fontSize}
onChange={(event) => {
const size = Number(event.target.value);
if (Number.isFinite(size) && size > 0) {
handleTextStyle({ fontSize: size });
}
}}
/>
</label>
<label className="cad-prop" title="문자 색상">
<span>색상</span>
<input
type="color"
value={textStyle.textColor}
onChange={(event) => handleTextStyle({ textColor: event.target.value })}
/>
</label>
</div>
<span className="cad-ribbon-group__label">문자</span>
</section>
</nav> </nav>
<aside className="cad-inspector controls" data-collapsed={panelCollapsed}> <aside className="cad-inspector controls" data-collapsed={panelCollapsed}>
@@ -1,15 +1,20 @@
import {Arc, type Box, Line, Point, type Segment} from '@flatten-js/core'; import { Arc, type Box, Line, Point, type Segment } from '@flatten-js/core';
import {uniqWith} from 'es-toolkit'; import { uniqWith } from 'es-toolkit';
import {type Shape, type SnapPoint, SnapPointType, type StartAndEndpointEntity} from '../App.types'; import {
import type {DrawController} from '../drawControllers/DrawController.ts'; type Shape,
import {getExportColor} from '../helpers/get-export-color'; type SnapPoint,
import {isPointEqual} from '../helpers/is-point-equal'; SnapPointType,
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts'; type StartAndEndpointEntity,
import {scalePoint} from '../helpers/scale-point'; } from '../App.types';
import {sortPointsOnArc} from '../helpers/sort-points-on-arc'; import type { DrawController } from '../drawControllers/DrawController.ts';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts'; import { getExportColor } from '../helpers/get-export-color';
import {type Entity, EntityName, type JsonEntity} from './Entity'; import { isPointEqual } from '../helpers/is-point-equal';
import type {LineEntity} from './LineEntity.ts'; import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import { scalePoint } from '../helpers/scale-point';
import { sortPointsOnArc } from '../helpers/sort-points-on-arc';
import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts';
import { type Entity, EntityName, type JsonEntity } from './Entity';
import type { LineEntity } from './LineEntity.ts';
export class ArcEntity implements Entity, StartAndEndpointEntity { export class ArcEntity implements Entity, StartAndEndpointEntity {
public id: string = crypto.randomUUID(); public id: string = crypto.randomUUID();
@@ -211,6 +216,7 @@ export class ArcEntity implements Entity, StartAndEndpointEntity {
type: EntityName.Arc, type: EntityName.Arc,
lineColor: this.lineColor, lineColor: this.lineColor,
lineWidth: this.lineWidth, lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId, layerId: this.layerId,
shapeData: { shapeData: {
center: { x: this.arc.center.x, y: this.arc.center.y }, center: { x: this.arc.center.x, y: this.arc.center.y },
@@ -248,6 +254,7 @@ export class ArcEntity implements Entity, StartAndEndpointEntity {
arcEntity.id = jsonEntity.id; arcEntity.id = jsonEntity.id;
arcEntity.lineColor = jsonEntity.lineColor; arcEntity.lineColor = jsonEntity.lineColor;
arcEntity.lineWidth = jsonEntity.lineWidth; arcEntity.lineWidth = jsonEntity.lineWidth;
arcEntity.lineDash = jsonEntity.lineDash;
return arcEntity; return arcEntity;
} }
@@ -1,12 +1,12 @@
import {Box, Point, Segment} from '@flatten-js/core'; import { Box, Point, Segment } from '@flatten-js/core';
import {max, min} from 'es-toolkit/compat'; import { max, min } from 'es-toolkit/compat';
import type {Shape, SnapPoint} from '../App.types'; import type { Shape, SnapPoint } from '../App.types';
import type {DrawController} from '../drawControllers/DrawController'; import type { DrawController } from '../drawControllers/DrawController';
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts'; import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import {scalePoint} from '../helpers/scale-point'; import { scalePoint } from '../helpers/scale-point';
import {getActiveLayerId} from '../state.ts'; import { getActiveLayerId } from '../state.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity'; import { type Entity, EntityName, type JsonEntity } from './Entity';
import type {LineEntity} from './LineEntity.ts'; import type { LineEntity } from './LineEntity.ts';
export class ArrowHeadEntity implements Entity { export class ArrowHeadEntity implements Entity {
public id: string = crypto.randomUUID(); public id: string = crypto.randomUUID();
@@ -138,6 +138,7 @@ export class ArrowHeadEntity implements Entity {
type: EntityName.ArrowHead, type: EntityName.ArrowHead,
lineColor: this.lineColor, lineColor: this.lineColor,
lineWidth: this.lineWidth, lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId, layerId: this.layerId,
shapeData: { shapeData: {
p1: { x: this.p1.x, y: this.p1.y }, p1: { x: this.p1.x, y: this.p1.y },
@@ -160,6 +161,7 @@ export class ArrowHeadEntity implements Entity {
lineEntity.id = jsonEntity.id; lineEntity.id = jsonEntity.id;
lineEntity.lineColor = jsonEntity.lineColor; lineEntity.lineColor = jsonEntity.lineColor;
lineEntity.lineWidth = jsonEntity.lineWidth; lineEntity.lineWidth = jsonEntity.lineWidth;
lineEntity.lineDash = jsonEntity.lineDash ?? [];
return lineEntity; return lineEntity;
} }
} }
@@ -1,12 +1,12 @@
import {type Box, Circle, Point, type Segment} from '@flatten-js/core'; import { type Box, Circle, Point, type Segment } from '@flatten-js/core';
import {type Shape, type SnapPoint, SnapPointType} from '../App.types'; import { type Shape, type SnapPoint, SnapPointType } from '../App.types';
import type {DrawController} from '../drawControllers/DrawController'; import type { DrawController } from '../drawControllers/DrawController';
import {getExportColor} from '../helpers/get-export-color'; import { getExportColor } from '../helpers/get-export-color';
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts'; import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import {scalePoint} from '../helpers/scale-point'; import { scalePoint } from '../helpers/scale-point';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts'; import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity'; import { type Entity, EntityName, type JsonEntity } from './Entity';
import type {LineEntity} from './LineEntity.ts'; import type { LineEntity } from './LineEntity.ts';
export class CircleEntity implements Entity { export class CircleEntity implements Entity {
public id: string = crypto.randomUUID(); public id: string = crypto.randomUUID();
@@ -168,6 +168,7 @@ export class CircleEntity implements Entity {
type: EntityName.Circle, type: EntityName.Circle,
lineColor: this.lineColor, lineColor: this.lineColor,
lineWidth: this.lineWidth, lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId, layerId: this.layerId,
shapeData: { shapeData: {
center: { x: this.circle.center.x, y: this.circle.center.y }, center: { x: this.circle.center.x, y: this.circle.center.y },
@@ -186,6 +187,7 @@ export class CircleEntity implements Entity {
circleEntity.id = jsonEntity.id; circleEntity.id = jsonEntity.id;
circleEntity.lineColor = jsonEntity.lineColor; circleEntity.lineColor = jsonEntity.lineColor;
circleEntity.lineWidth = jsonEntity.lineWidth; circleEntity.lineWidth = jsonEntity.lineWidth;
circleEntity.lineDash = jsonEntity.lineDash;
return circleEntity; return circleEntity;
} }
@@ -1,14 +1,14 @@
import type {Box, Point, Segment} from '@flatten-js/core'; import type { Box, Point, Segment } from '@flatten-js/core';
import type {Shape, SnapPoint} from '../App.types'; import type { Shape, SnapPoint } from '../App.types';
import type {DrawController} from '../drawControllers/DrawController.ts'; import type { DrawController } from '../drawControllers/DrawController.ts';
import type {ArcJsonData} from './ArcEntity'; import type { ArcJsonData } from './ArcEntity';
import type {ArrowHeadJsonData} from './ArrowHeadEntity.ts'; import type { ArrowHeadJsonData } from './ArrowHeadEntity.ts';
import type {CircleJsonData} from './CircleEntity'; import type { CircleJsonData } from './CircleEntity';
import type {ImageJsonData} from './ImageEntity'; import type { ImageJsonData } from './ImageEntity';
import type {LineEntity, LineJsonData} from './LineEntity'; import type { LineEntity, LineJsonData } from './LineEntity';
import type {PointJsonData} from './PointEntity'; import type { PointJsonData } from './PointEntity';
import type {RectangleJsonData} from './RectangleEntity'; import type { RectangleJsonData } from './RectangleEntity';
import type {TextJsonData} from './TextEntity.ts'; import type { TextJsonData } from './TextEntity.ts';
export interface Entity { export interface Entity {
// Random uuid generated when the Entity is created // Random uuid generated when the Entity is created
@@ -74,6 +74,7 @@ export interface JsonEntity<TShapeJsonData = ShapeJsonData> {
type: EntityName; type: EntityName;
lineColor: string; lineColor: string;
lineWidth: number; lineWidth: number;
lineDash?: number[];
layerId: string; layerId: string;
shapeData: TShapeJsonData | null; shapeData: TShapeJsonData | null;
children?: JsonEntity<ShapeJsonData>[]; children?: JsonEntity<ShapeJsonData>[];
@@ -1,16 +1,16 @@
import type * as Flatten from '@flatten-js/core'; import type * as Flatten from '@flatten-js/core';
import {type Box, Point, Polygon, Relations, type Segment, Vector} from '@flatten-js/core'; import { type Box, Point, Polygon, Relations, type Segment, Vector } from '@flatten-js/core';
import {type Shape, type SnapPoint, SnapPointType} from '../App.types'; import { type Shape, type SnapPoint, SnapPointType } from '../App.types';
import type {DrawController} from '../drawControllers/DrawController.ts'; import type { DrawController } from '../drawControllers/DrawController.ts';
import {twoPointBoxToPolygon} from '../helpers/box-to-polygon'; import { twoPointBoxToPolygon } from '../helpers/box-to-polygon';
import {getExportColor} from '../helpers/get-export-color'; import { getExportColor } from '../helpers/get-export-color';
import {mirrorAngleOverAxis} from '../helpers/mirror-angle-over-axis.ts'; import { mirrorAngleOverAxis } from '../helpers/mirror-angle-over-axis.ts';
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts'; import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import {polygonToSegments} from '../helpers/polygon-to-segments'; import { polygonToSegments } from '../helpers/polygon-to-segments';
import {scalePoint} from '../helpers/scale-point'; import { scalePoint } from '../helpers/scale-point';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts'; import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity'; import { type Entity, EntityName, type JsonEntity } from './Entity';
import type {LineEntity} from './LineEntity.ts'; import type { LineEntity } from './LineEntity.ts';
export class ImageEntity implements Entity { export class ImageEntity implements Entity {
public id: string = crypto.randomUUID(); public id: string = crypto.randomUUID();
@@ -213,6 +213,7 @@ export class ImageEntity implements Entity {
type: EntityName.Image, type: EntityName.Image,
lineColor: this.lineColor, lineColor: this.lineColor,
lineWidth: this.lineWidth, lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId, layerId: this.layerId,
shapeData: { shapeData: {
points: this.polygon.vertices.map((vertex) => ({ points: this.polygon.vertices.map((vertex) => ({
@@ -241,6 +242,7 @@ export class ImageEntity implements Entity {
rectangleEntity.id = jsonEntity.id; rectangleEntity.id = jsonEntity.id;
rectangleEntity.lineColor = jsonEntity.lineColor; rectangleEntity.lineColor = jsonEntity.lineColor;
rectangleEntity.lineWidth = jsonEntity.lineWidth; rectangleEntity.lineWidth = jsonEntity.lineWidth;
rectangleEntity.lineDash = jsonEntity.lineDash;
return rectangleEntity; return rectangleEntity;
} }
} }
@@ -1,15 +1,20 @@
import {type Box, Point, Segment} from '@flatten-js/core'; import { type Box, Point, Segment } from '@flatten-js/core';
import {sortBy, uniqWith} from 'es-toolkit'; import { sortBy, uniqWith } from 'es-toolkit';
import {type Shape, type SnapPoint, SnapPointType, type StartAndEndpointEntity} from '../App.types'; import {
import type {DrawController} from '../drawControllers/DrawController'; type Shape,
import {pointDistance} from '../helpers/distance-between-points'; type SnapPoint,
import {getAngleWithXAxis} from '../helpers/get-angle-with-x-axis.ts'; SnapPointType,
import {getExportColor} from '../helpers/get-export-color'; type StartAndEndpointEntity,
import {isPointEqual} from '../helpers/is-point-equal'; } from '../App.types';
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts'; import type { DrawController } from '../drawControllers/DrawController';
import {scalePoint} from '../helpers/scale-point'; import { pointDistance } from '../helpers/distance-between-points';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts'; import { getAngleWithXAxis } from '../helpers/get-angle-with-x-axis.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity'; import { getExportColor } from '../helpers/get-export-color';
import { isPointEqual } from '../helpers/is-point-equal';
import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import { scalePoint } from '../helpers/scale-point';
import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts';
import { type Entity, EntityName, type JsonEntity } from './Entity';
export class LineEntity implements Entity, StartAndEndpointEntity { export class LineEntity implements Entity, StartAndEndpointEntity {
public id: string = crypto.randomUUID(); public id: string = crypto.randomUUID();
@@ -174,6 +179,7 @@ export class LineEntity implements Entity, StartAndEndpointEntity {
type: EntityName.Line, type: EntityName.Line,
lineColor: this.lineColor, lineColor: this.lineColor,
lineWidth: this.lineWidth, lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId, layerId: this.layerId,
shapeData: { shapeData: {
startPoint: { startPoint: {
@@ -202,6 +208,7 @@ export class LineEntity implements Entity, StartAndEndpointEntity {
lineEntity.id = jsonEntity.id; lineEntity.id = jsonEntity.id;
lineEntity.lineColor = jsonEntity.lineColor; lineEntity.lineColor = jsonEntity.lineColor;
lineEntity.lineWidth = jsonEntity.lineWidth; lineEntity.lineWidth = jsonEntity.lineWidth;
lineEntity.lineDash = jsonEntity.lineDash;
return lineEntity; return lineEntity;
} }
@@ -1,6 +1,6 @@
import {Box, Line, Point, Segment, Vector} from '@flatten-js/core'; import { Box, Line, Point, Segment, Vector } from '@flatten-js/core';
import {minBy, round} from 'es-toolkit'; import { minBy, round } from 'es-toolkit';
import {max, min} from 'es-toolkit/compat'; import { max, min } from 'es-toolkit/compat';
import { import {
ARROW_HEAD_LENGTH, ARROW_HEAD_LENGTH,
ARROW_HEAD_WIDTH, ARROW_HEAD_WIDTH,
@@ -12,15 +12,32 @@ import {
MEASUREMENT_ORIGIN_MARGIN, MEASUREMENT_ORIGIN_MARGIN,
TO_RADIANS, TO_RADIANS,
} from '../App.consts'; } from '../App.consts';
import type {Shape, SnapPoint} from '../App.types'; import type { Shape, SnapPoint } from '../App.types';
import type {DrawController} from '../drawControllers/DrawController'; import type { DrawController } from '../drawControllers/DrawController';
import {pointDistance} from '../helpers/distance-between-points'; import { pointDistance } from '../helpers/distance-between-points';
import {isPointEqual} from '../helpers/is-point-equal'; import { isPointEqual } from '../helpers/is-point-equal';
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts'; import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import {scalePoint} from '../helpers/scale-point'; import { scalePoint } from '../helpers/scale-point';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts'; import {
import {type Entity, EntityName, type JsonEntity} from './Entity'; getActiveLayerId,
import type {LineEntity} from './LineEntity.ts'; getScreenCanvasDrawController,
isEntityHighlighted,
isEntitySelected,
} from '../state.ts';
import { type Entity, EntityName, type JsonEntity } from './Entity';
import type { LineEntity } from './LineEntity.ts';
/**
* 치수 상수는 화면 픽셀 기준이므로 현재 줌 배율(px/world)로 나눠 세계좌표 길이로 바꾼다.
* 컨트롤러가 아직 없는 환경(단위 테스트 등)에서는 1을 반환해 상수를 그대로 쓴다.
*/
function annotationWorldFactor(): number {
try {
return getScreenCanvasDrawController().getScreenScale() || 1;
} catch {
return 1;
}
}
export class MeasurementEntity implements Entity { export class MeasurementEntity implements Entity {
public id: string = crypto.randomUUID(); public id: string = crypto.randomUUID();
@@ -77,30 +94,41 @@ export class MeasurementEntity implements Entity {
.clone() .clone()
.translate(vectorPerpendicularFromLineTowardsOffsetPoint); .translate(vectorPerpendicularFromLineTowardsOffsetPoint);
// Screen-pixel constants are converted to world units so annotation size stays zoom-independent
const worldFactor = annotationWorldFactor();
// Start of the perpendicular lines // Start of the perpendicular lines
const offsetStartPointMargin = this.startPoint const offsetStartPointMargin = this.startPoint
.clone() .clone()
.translate( .translate(
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_ORIGIN_MARGIN) vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(
MEASUREMENT_ORIGIN_MARGIN / worldFactor
)
); );
const offsetEndPointMargin = this.endPoint const offsetEndPointMargin = this.endPoint
.clone() .clone()
.translate( .translate(
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_ORIGIN_MARGIN) vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(
MEASUREMENT_ORIGIN_MARGIN / worldFactor
)
); );
// End of the perpendicular lines // End of the perpendicular lines
const offsetStartPointExtend = offsetStartPoint const offsetStartPointExtend = offsetStartPoint
.clone() .clone()
.translate( .translate(
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_EXTENSION_LENGTH) vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(
MEASUREMENT_EXTENSION_LENGTH / worldFactor
)
); );
const offsetEndPointExtend = offsetEndPoint const offsetEndPointExtend = offsetEndPoint
.clone() .clone()
.translate( .translate(
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_EXTENSION_LENGTH) vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(
MEASUREMENT_EXTENSION_LENGTH / worldFactor
)
); );
// Location for label // Location for label
@@ -108,8 +136,8 @@ export class MeasurementEntity implements Entity {
(offsetStartPoint.x + offsetEndPoint.x) / 2, (offsetStartPoint.x + offsetEndPoint.x) / 2,
(offsetStartPoint.y + offsetEndPoint.y) / 2 (offsetStartPoint.y + offsetEndPoint.y) / 2
); );
const textHeight = MEASUREMENT_FONT_SIZE; const textHeight = MEASUREMENT_FONT_SIZE / worldFactor;
const totalOffset = MEASUREMENT_LABEL_OFFSET + textHeight / 2; const totalOffset = MEASUREMENT_LABEL_OFFSET / worldFactor + textHeight / 2;
const midpointMeasurementLineOffset = midpointMeasurementLine const midpointMeasurementLineOffset = midpointMeasurementLine
.clone() .clone()
.translate(vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(totalOffset)); .translate(vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(totalOffset));
@@ -163,20 +191,21 @@ export class MeasurementEntity implements Entity {
isHighlighted: boolean, isHighlighted: boolean,
isSelected: boolean isSelected: boolean
): void => { ): void => {
const screenScale = drawController.getScreenScale(); // Arrow heads keep a constant on-screen size: divide pixel constants by zoom (px/world)
const worldFactor = drawController.getScreenScale() || 1;
const vectorFromEndToStart = new Vector(endPoint, startPoint); const vectorFromEndToStart = new Vector(endPoint, startPoint);
const vectorFromEndToStartUnit = vectorFromEndToStart.normalize(); const vectorFromEndToStartUnit = vectorFromEndToStart.normalize();
const baseOfArrow = endPoint const baseOfArrow = endPoint
.clone() .clone()
.translate(vectorFromEndToStartUnit.multiply(ARROW_HEAD_LENGTH * screenScale)); .translate(vectorFromEndToStartUnit.multiply(ARROW_HEAD_LENGTH / worldFactor));
const perpendicularVector1 = vectorFromEndToStartUnit.rotate(90 * TO_RADIANS); const perpendicularVector1 = vectorFromEndToStartUnit.rotate(90 * TO_RADIANS);
const perpendicularVector2 = vectorFromEndToStartUnit.rotate(-90 * TO_RADIANS); const perpendicularVector2 = vectorFromEndToStartUnit.rotate(-90 * TO_RADIANS);
const leftCornerOfArrow = baseOfArrow const leftCornerOfArrow = baseOfArrow
.clone() .clone()
.translate(perpendicularVector1.multiply(ARROW_HEAD_WIDTH * screenScale)); .translate(perpendicularVector1.multiply(ARROW_HEAD_WIDTH / worldFactor));
const rightCornerOfArrow = baseOfArrow const rightCornerOfArrow = baseOfArrow
.clone() .clone()
.translate(perpendicularVector2.multiply(ARROW_HEAD_WIDTH * screenScale)); .translate(perpendicularVector2.multiply(ARROW_HEAD_WIDTH / worldFactor));
drawController.setLineStyles( drawController.setLineStyles(
isHighlighted, isHighlighted,
@@ -268,7 +297,7 @@ export class MeasurementEntity implements Entity {
drawController.drawText(distance, midpointMeasurementLineOffset, { drawController.drawText(distance, midpointMeasurementLineOffset, {
textAlign: 'center', textAlign: 'center',
textDirection: finalTextDirection, textDirection: finalTextDirection,
fontSize: MEASUREMENT_FONT_SIZE, fontSize: MEASUREMENT_FONT_SIZE / (drawController.getScreenScale() || 1),
textColor: this.lineColor, textColor: this.lineColor,
}); });
} }
@@ -374,9 +403,10 @@ export class MeasurementEntity implements Entity {
const distance = String( const distance = String(
round(pointDistance(this.startPoint, this.endPoint), MEASUREMENT_DECIMAL_PLACES) round(pointDistance(this.startPoint, this.endPoint), MEASUREMENT_DECIMAL_PLACES)
); );
const textHeight = MEASUREMENT_FONT_SIZE; const worldFactor = annotationWorldFactor();
const textHeight = MEASUREMENT_FONT_SIZE / worldFactor;
// Estimate width: textString.length * fontSize * aspectRatioFactor // Estimate width: textString.length * fontSize * aspectRatioFactor
const textWidth = distance.length * MEASUREMENT_FONT_SIZE * 0.6; const textWidth = (distance.length * MEASUREMENT_FONT_SIZE * 0.6) / worldFactor;
const { midpointMeasurementLineOffset, normalUnit } = drawPoints; const { midpointMeasurementLineOffset, normalUnit } = drawPoints;
@@ -532,6 +562,7 @@ export class MeasurementEntity implements Entity {
type: EntityName.Measurement, type: EntityName.Measurement,
lineColor: this.lineColor, lineColor: this.lineColor,
lineWidth: this.lineWidth, lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId, layerId: this.layerId,
shapeData: { shapeData: {
startPoint: { x: this.startPoint.x, y: this.startPoint.y }, startPoint: { x: this.startPoint.x, y: this.startPoint.y },
@@ -565,6 +596,7 @@ export class MeasurementEntity implements Entity {
measurementEntity.id = jsonEntity.id; measurementEntity.id = jsonEntity.id;
measurementEntity.lineColor = jsonEntity.lineColor; measurementEntity.lineColor = jsonEntity.lineColor;
measurementEntity.lineWidth = jsonEntity.lineWidth; measurementEntity.lineWidth = jsonEntity.lineWidth;
measurementEntity.lineDash = jsonEntity.lineDash;
return measurementEntity; return measurementEntity;
} }
} }
@@ -1,13 +1,13 @@
import type * as Flatten from '@flatten-js/core'; import type * as Flatten from '@flatten-js/core';
import {Box, Point, type Segment} from '@flatten-js/core'; import { Box, Point, type Segment } from '@flatten-js/core';
import {type Shape, type SnapPoint, SnapPointType} from '../App.types'; import { type Shape, type SnapPoint, SnapPointType } from '../App.types';
import type {DrawController} from '../drawControllers/DrawController'; import type { DrawController } from '../drawControllers/DrawController';
import {getExportColor} from '../helpers/get-export-color'; import { getExportColor } from '../helpers/get-export-color';
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts'; import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import {scalePoint} from '../helpers/scale-point'; import { scalePoint } from '../helpers/scale-point';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts'; import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity'; import { type Entity, EntityName, type JsonEntity } from './Entity';
import type {LineEntity} from './LineEntity.ts'; import type { LineEntity } from './LineEntity.ts';
export class PointEntity implements Entity { export class PointEntity implements Entity {
public id: string = crypto.randomUUID(); public id: string = crypto.randomUUID();
@@ -124,6 +124,7 @@ export class PointEntity implements Entity {
type: EntityName.Point, type: EntityName.Point,
lineColor: this.lineColor, lineColor: this.lineColor,
lineWidth: this.lineWidth, lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId, layerId: this.layerId,
shapeData: { shapeData: {
point: { point: {
@@ -143,6 +144,7 @@ export class PointEntity implements Entity {
lineEntity.id = jsonEntity.id; lineEntity.id = jsonEntity.id;
lineEntity.lineColor = jsonEntity.lineColor; lineEntity.lineColor = jsonEntity.lineColor;
lineEntity.lineWidth = jsonEntity.lineWidth; lineEntity.lineWidth = jsonEntity.lineWidth;
lineEntity.lineDash = jsonEntity.lineDash;
return lineEntity; return lineEntity;
} }
} }
@@ -1,14 +1,14 @@
import type * as Flatten from '@flatten-js/core'; import type * as Flatten from '@flatten-js/core';
import {Box, type Point, type Segment} from '@flatten-js/core'; import { Box, type Point, type Segment } from '@flatten-js/core';
import {mapLimit} from 'blend-promise-utils'; import { mapLimit } from 'blend-promise-utils';
import {compact, maxBy} from 'es-toolkit'; import { compact, maxBy } from 'es-toolkit';
import {minBy} from 'es-toolkit/compat'; import { minBy } from 'es-toolkit/compat';
import type {Shape, SnapPoint} from '../App.types'; import type { Shape, SnapPoint } from '../App.types';
import type {DrawController} from '../drawControllers/DrawController'; import type { DrawController } from '../drawControllers/DrawController';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts'; import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts';
import {ArcEntity, type ArcJsonData} from './ArcEntity.ts'; import { ArcEntity, type ArcJsonData } from './ArcEntity.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity'; import { type Entity, EntityName, type JsonEntity } from './Entity';
import {LineEntity, type LineJsonData} from './LineEntity.ts'; import { LineEntity, type LineJsonData } from './LineEntity.ts';
export class PolyLineEntity implements Entity { export class PolyLineEntity implements Entity {
public id: string = crypto.randomUUID(); public id: string = crypto.randomUUID();
@@ -133,6 +133,7 @@ export class PolyLineEntity implements Entity {
type: EntityName.PolyLine, type: EntityName.PolyLine,
lineColor: this.lineColor, lineColor: this.lineColor,
lineWidth: this.lineWidth, lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId, layerId: this.layerId,
shapeData: null, shapeData: null,
children: compact(await mapLimit(this.entities, 20, (entity) => entity.toJson())), children: compact(await mapLimit(this.entities, 20, (entity) => entity.toJson())),
@@ -178,6 +179,7 @@ export class PolyLineEntity implements Entity {
polyLineEntity.id = jsonEntity.id; polyLineEntity.id = jsonEntity.id;
polyLineEntity.lineColor = jsonEntity.lineColor; polyLineEntity.lineColor = jsonEntity.lineColor;
polyLineEntity.lineWidth = jsonEntity.lineWidth; polyLineEntity.lineWidth = jsonEntity.lineWidth;
polyLineEntity.lineDash = jsonEntity.lineDash;
return polyLineEntity; return polyLineEntity;
} }
} }
@@ -1,15 +1,15 @@
import type * as Flatten from '@flatten-js/core'; import type * as Flatten from '@flatten-js/core';
import {type Box, Point, Polygon, Relations, type Segment, Vector} from '@flatten-js/core'; import { type Box, Point, Polygon, Relations, type Segment, Vector } from '@flatten-js/core';
import {type Shape, type SnapPoint, SnapPointType} from '../App.types'; import { type Shape, type SnapPoint, SnapPointType } from '../App.types';
import type {DrawController} from '../drawControllers/DrawController'; import type { DrawController } from '../drawControllers/DrawController';
import {twoPointBoxToPolygon} from '../helpers/box-to-polygon'; import { twoPointBoxToPolygon } from '../helpers/box-to-polygon';
import {getExportColor} from '../helpers/get-export-color'; import { getExportColor } from '../helpers/get-export-color';
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts'; import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import {polygonToSegments} from '../helpers/polygon-to-segments'; import { polygonToSegments } from '../helpers/polygon-to-segments';
import {scalePoint} from '../helpers/scale-point'; import { scalePoint } from '../helpers/scale-point';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts'; import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity'; import { type Entity, EntityName, type JsonEntity } from './Entity';
import type {LineEntity} from './LineEntity.ts'; import type { LineEntity } from './LineEntity.ts';
export class RectangleEntity implements Entity { export class RectangleEntity implements Entity {
public id: string = crypto.randomUUID(); public id: string = crypto.randomUUID();
@@ -176,6 +176,7 @@ export class RectangleEntity implements Entity {
type: EntityName.Rectangle, type: EntityName.Rectangle,
lineColor: this.lineColor, lineColor: this.lineColor,
lineWidth: this.lineWidth, lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId, layerId: this.layerId,
shapeData: { shapeData: {
points: this.polygon.vertices.map((vertex) => ({ points: this.polygon.vertices.map((vertex) => ({
@@ -202,6 +203,7 @@ export class RectangleEntity implements Entity {
rectangleEntity.id = jsonEntity.id; rectangleEntity.id = jsonEntity.id;
rectangleEntity.lineColor = jsonEntity.lineColor; rectangleEntity.lineColor = jsonEntity.lineColor;
rectangleEntity.lineWidth = jsonEntity.lineWidth; rectangleEntity.lineWidth = jsonEntity.lineWidth;
rectangleEntity.lineDash = jsonEntity.lineDash;
return rectangleEntity; return rectangleEntity;
} }
} }
@@ -1,12 +1,12 @@
import {Box, Point, type Segment, Vector} from '@flatten-js/core'; import { Box, Point, type Segment, Vector } from '@flatten-js/core';
import {cloneDeep} from 'es-toolkit/compat'; import { cloneDeep } from 'es-toolkit/compat';
import type {Shape, SnapPoint} from '../App.types'; import type { Shape, SnapPoint } from '../App.types';
import {DEFAULT_TEXT_OPTIONS, type DrawController} from '../drawControllers/DrawController'; import { DEFAULT_TEXT_OPTIONS, type DrawController } from '../drawControllers/DrawController';
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts'; import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import {scalePoint} from '../helpers/scale-point.ts'; import { scalePoint } from '../helpers/scale-point.ts';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts'; import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity'; import { type Entity, EntityName, type JsonEntity } from './Entity';
import type {LineEntity} from './LineEntity.ts'; import type { LineEntity } from './LineEntity.ts';
export interface TextOptions { export interface TextOptions {
textDirection: Vector; textDirection: Vector;
@@ -101,6 +101,14 @@ export class TextEntity implements Entity {
); );
} }
public getTextOptions(): TextOptions {
return this.options;
}
public setTextOptions(newOptions: Partial<Omit<TextOptions, 'textDirection'>>): void {
Object.assign(this.options, newOptions);
}
public getShape(): Shape | null { public getShape(): Shape | null {
return null; // TODO see why we need to get the shape out of an entity return null; // TODO see why we need to get the shape out of an entity
} }
@@ -141,6 +149,7 @@ export class TextEntity implements Entity {
type: EntityName.Text, type: EntityName.Text,
lineColor: this.lineColor, lineColor: this.lineColor,
lineWidth: this.lineWidth, lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId, layerId: this.layerId,
shapeData: { shapeData: {
label: this.label, label: this.label,
@@ -181,6 +190,7 @@ export class TextEntity implements Entity {
textEntity.id = jsonEntity.id; textEntity.id = jsonEntity.id;
textEntity.lineColor = jsonEntity.lineColor; textEntity.lineColor = jsonEntity.lineColor;
textEntity.lineWidth = jsonEntity.lineWidth; textEntity.lineWidth = jsonEntity.lineWidth;
textEntity.lineDash = jsonEntity.lineDash ?? [];
return textEntity; return textEntity;
} }
} }
@@ -26,6 +26,8 @@ export enum StateVariable {
lastDrawTimestamp = 'lastDrawTimestamp', lastDrawTimestamp = 'lastDrawTimestamp',
activeLineColor = 'activeLineColor', activeLineColor = 'activeLineColor',
activeLineWidth = 'activeLineWidth', activeLineWidth = 'activeLineWidth',
activeLineDash = 'activeLineDash',
activeTextStyle = 'activeTextStyle',
layers = 'layers', layers = 'layers',
} }
+37 -2
View File
@@ -121,15 +121,29 @@ let hoveredSnapPoints: HoverPoint[] = [];
let lastDrawTimestamp: DOMHighResTimeStamp = 0; let lastDrawTimestamp: DOMHighResTimeStamp = 0;
/** /**
* Active line color * Active line color (7-char hex so <input type="color"> can consume it directly)
*/ */
let activeLineColor = '#fff'; let activeLineColor = '#ffffff';
/** /**
* Active line width * Active line width
*/ */
let activeLineWidth = 1; let activeLineWidth = 1;
/**
* Active line dash pattern (screen px). undefined → solid line
*/
let activeLineDash: number[] | undefined = undefined;
/**
* Active text style defaults applied to newly created text and selected text entities
*/
let activeTextStyle = {
fontFamily: 'Noto Sans KR',
fontSize: 16,
textColor: '#ffffff',
};
/** /**
* Layers that can contain entities * Layers that can contain entities
*/ */
@@ -170,6 +184,8 @@ export const getHoveredSnapPoints = () => hoveredSnapPoints;
export const getLastDrawTimestamp = () => lastDrawTimestamp; export const getLastDrawTimestamp = () => lastDrawTimestamp;
export const getActiveLineColor = () => activeLineColor; export const getActiveLineColor = () => activeLineColor;
export const getActiveLineWidth = () => activeLineWidth; export const getActiveLineWidth = () => activeLineWidth;
export const getActiveLineDash = () => activeLineDash;
export const getActiveTextStyle = () => activeTextStyle;
export const getScreenCanvasDrawController = (): ScreenCanvasDrawController => { export const getScreenCanvasDrawController = (): ScreenCanvasDrawController => {
if (!screenCanvasDrawController) { if (!screenCanvasDrawController) {
throw new Error('getScreenCanvasDrawController() returned null'); throw new Error('getScreenCanvasDrawController() returned null');
@@ -331,6 +347,23 @@ export const setActiveLineWidth = (newWidth: number, triggerReact = true) => {
triggerReactUpdate(StateVariable.activeLineWidth); triggerReactUpdate(StateVariable.activeLineWidth);
} }
}; };
export const setActiveLineDash = (newDash: number[] | undefined, triggerReact = true) => {
activeLineDash = newDash;
if (triggerReact) {
triggerReactUpdate(StateVariable.activeLineDash);
}
};
export const setActiveTextStyle = (
newStyle: Partial<typeof activeTextStyle>,
triggerReact = true
) => {
activeTextStyle = { ...activeTextStyle, ...newStyle };
if (triggerReact) {
triggerReactUpdate(StateVariable.activeTextStyle);
}
};
export const setLayers = (newLayers: Layer[], triggerReact = true) => { export const setLayers = (newLayers: Layer[], triggerReact = true) => {
layers = newLayers; layers = newLayers;
@@ -379,6 +412,8 @@ const reactStateVariables: StateVariable[] = [
StateVariable.angleStep, StateVariable.angleStep,
StateVariable.activeLineColor, StateVariable.activeLineColor,
StateVariable.activeLineWidth, StateVariable.activeLineWidth,
StateVariable.activeLineDash,
StateVariable.activeTextStyle,
StateVariable.screenZoom, StateVariable.screenZoom,
StateVariable.layers, StateVariable.layers,
]; ];
@@ -1,161 +1,164 @@
import {CircleEntity} from '../entities/CircleEntity'; import { CircleEntity } from '../entities/CircleEntity';
import type {Point} from '@flatten-js/core'; import type { Point } from '@flatten-js/core';
import { import {
addEntities, addEntities,
getActiveLayerId, getActiveLayerId,
getActiveLineColor, getActiveLineColor,
getActiveLineWidth, getActiveLineDash,
setAngleGuideOriginPoint, getActiveLineWidth,
setGhostHelperEntities, setAngleGuideOriginPoint,
setSelectedEntityIds, setGhostHelperEntities,
setShouldDrawHelpers, setSelectedEntityIds,
setShouldDrawHelpers,
} from '../state'; } from '../state';
import type {DrawEvent, PointInputEvent, StateEvent, ToolContext,} from './tool.types'; import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types';
import {Tool} from '../tools'; import { Tool } from '../tools';
import {assign, createMachine} from 'xstate'; import { assign, createMachine } from 'xstate';
import {pointDistance} from '../helpers/distance-between-points'; import { pointDistance } from '../helpers/distance-between-points';
import {LineState} from './line-tool.ts'; import { LineState } from './line-tool.ts';
import {getPointFromEvent} from '../helpers/get-point-from-event.ts'; import { getPointFromEvent } from '../helpers/get-point-from-event.ts';
export interface CircleContext extends ToolContext { export interface CircleContext extends ToolContext {
centerPoint: Point | null; centerPoint: Point | null;
} }
export enum CircleState { export enum CircleState {
WAITING_FOR_CENTER_POINT = 'WAITING_FOR_CENTER_POINT', WAITING_FOR_CENTER_POINT = 'WAITING_FOR_CENTER_POINT',
WAITING_FOR_POINT_ON_CIRCLE = 'WAITING_FOR_POINT_ON_CIRCLE', WAITING_FOR_POINT_ON_CIRCLE = 'WAITING_FOR_POINT_ON_CIRCLE',
INIT = 'INIT', INIT = 'INIT',
} }
export enum CircleAction { export enum CircleAction {
INIT_CIRCLE_TOOL = 'INIT_CIRCLE_TOOL', INIT_CIRCLE_TOOL = 'INIT_CIRCLE_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT', RECORD_START_POINT = 'RECORD_START_POINT',
DRAW_TEMP_CIRCLE = 'DRAW_TEMP_CIRCLE', DRAW_TEMP_CIRCLE = 'DRAW_TEMP_CIRCLE',
DRAW_FINAL_CIRCLE = 'DRAW_FINAL_CIRCLE', DRAW_FINAL_CIRCLE = 'DRAW_FINAL_CIRCLE',
} }
export const circleToolStateMachine = createMachine( export const circleToolStateMachine = createMachine(
{ {
types: {} as { types: {} as {
context: CircleContext; context: CircleContext;
events: StateEvent; events: StateEvent;
}, },
context: { context: {
centerPoint: null, centerPoint: null,
type: Tool.CIRCLE, type: Tool.CIRCLE,
}, },
initial: CircleState.INIT, initial: CircleState.INIT,
states: { states: {
[CircleState.INIT]: { [CircleState.INIT]: {
description: 'Initializing the circle tool', description: 'Initializing the circle tool',
always: { always: {
actions: CircleAction.INIT_CIRCLE_TOOL, actions: CircleAction.INIT_CIRCLE_TOOL,
target: CircleState.WAITING_FOR_CENTER_POINT, target: CircleState.WAITING_FOR_CENTER_POINT,
}, },
}, },
[CircleState.WAITING_FOR_CENTER_POINT]: { [CircleState.WAITING_FOR_CENTER_POINT]: {
description: 'Select the center point of the circle tool', description: 'Select the center point of the circle tool',
meta: { meta: {
instructions: 'Select the center point of the circle', instructions: 'Select the center point of the circle',
}, },
on: { on: {
MOUSE_CLICK: { MOUSE_CLICK: {
actions: CircleAction.RECORD_START_POINT, actions: CircleAction.RECORD_START_POINT,
target: CircleState.WAITING_FOR_POINT_ON_CIRCLE, target: CircleState.WAITING_FOR_POINT_ON_CIRCLE,
}, },
ABSOLUTE_POINT_INPUT: { ABSOLUTE_POINT_INPUT: {
actions: CircleAction.RECORD_START_POINT, actions: CircleAction.RECORD_START_POINT,
target: CircleState.WAITING_FOR_POINT_ON_CIRCLE, target: CircleState.WAITING_FOR_POINT_ON_CIRCLE,
}, },
}, },
}, },
[CircleState.WAITING_FOR_POINT_ON_CIRCLE]: { [CircleState.WAITING_FOR_POINT_ON_CIRCLE]: {
description: 'Select a point on the circle', description: 'Select a point on the circle',
meta: { meta: {
instructions: 'Select the point on the circle', instructions: 'Select the point on the circle',
}, },
on: { on: {
DRAW: { DRAW: {
actions: CircleAction.DRAW_TEMP_CIRCLE, actions: CircleAction.DRAW_TEMP_CIRCLE,
}, },
MOUSE_CLICK: { MOUSE_CLICK: {
actions: CircleAction.DRAW_FINAL_CIRCLE, actions: CircleAction.DRAW_FINAL_CIRCLE,
target: CircleState.INIT, target: CircleState.INIT,
}, },
NUMBER_INPUT: { NUMBER_INPUT: {
actions: CircleAction.DRAW_FINAL_CIRCLE, actions: CircleAction.DRAW_FINAL_CIRCLE,
target: LineState.INIT, target: LineState.INIT,
}, },
ABSOLUTE_POINT_INPUT: { ABSOLUTE_POINT_INPUT: {
actions: CircleAction.DRAW_FINAL_CIRCLE, actions: CircleAction.DRAW_FINAL_CIRCLE,
target: LineState.INIT, target: LineState.INIT,
}, },
RELATIVE_POINT_INPUT: { RELATIVE_POINT_INPUT: {
actions: CircleAction.DRAW_FINAL_CIRCLE, actions: CircleAction.DRAW_FINAL_CIRCLE,
target: LineState.INIT, target: LineState.INIT,
}, },
ESC: { ESC: {
target: CircleState.INIT, target: CircleState.INIT,
}, },
}, },
}, },
}, },
}, },
{ {
actions: { actions: {
[CircleAction.INIT_CIRCLE_TOOL]: assign(() => { [CircleAction.INIT_CIRCLE_TOOL]: assign(() => {
setShouldDrawHelpers(true); setShouldDrawHelpers(true);
setGhostHelperEntities([]); setGhostHelperEntities([]);
setSelectedEntityIds([]); setSelectedEntityIds([]);
setAngleGuideOriginPoint(null); setAngleGuideOriginPoint(null);
return { return {
centerPoint: null, centerPoint: null,
}; };
}), }),
[CircleAction.RECORD_START_POINT]: assign(({ event }) => { [CircleAction.RECORD_START_POINT]: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent); const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint); setAngleGuideOriginPoint(startPoint);
return { return {
centerPoint: startPoint, centerPoint: startPoint,
}; };
}), }),
[CircleAction.DRAW_TEMP_CIRCLE]: ({ context, event }) => { [CircleAction.DRAW_TEMP_CIRCLE]: ({ context, event }) => {
const activeCircle = new CircleEntity( const activeCircle = new CircleEntity(
getActiveLayerId(), getActiveLayerId(),
context.centerPoint as Point, context.centerPoint as Point,
pointDistance( pointDistance(
(event as DrawEvent).drawController.getWorldMouseLocation(), (event as DrawEvent).drawController.getWorldMouseLocation(),
context.centerPoint as Point, context.centerPoint as Point
), )
); );
activeCircle.lineColor = getActiveLineColor(); activeCircle.lineColor = getActiveLineColor();
activeCircle.lineWidth = getActiveLineWidth(); activeCircle.lineWidth = getActiveLineWidth();
setGhostHelperEntities([activeCircle]); activeCircle.lineDash = getActiveLineDash();
}, setGhostHelperEntities([activeCircle]);
[CircleAction.DRAW_FINAL_CIRCLE]: assign(({ context, event }) => { },
if (!context.centerPoint) { [CircleAction.DRAW_FINAL_CIRCLE]: assign(({ context, event }) => {
throw new Error( if (!context.centerPoint) {
'Trying to DRAW_FINAL_CIRCLE when centerPoint is not yet defined in circle tool', throw new Error(
); 'Trying to DRAW_FINAL_CIRCLE when centerPoint is not yet defined in circle tool'
} );
const pointOnCircle: Point = getPointFromEvent( }
context.centerPoint, const pointOnCircle: Point = getPointFromEvent(
event as PointInputEvent, context.centerPoint,
); event as PointInputEvent
const activeCircle = new CircleEntity( );
getActiveLayerId(), const activeCircle = new CircleEntity(
context.centerPoint as Point, getActiveLayerId(),
pointDistance(pointOnCircle, context.centerPoint as Point), context.centerPoint as Point,
); pointDistance(pointOnCircle, context.centerPoint as Point)
activeCircle.lineColor = getActiveLineColor(); );
activeCircle.lineWidth = getActiveLineWidth(); activeCircle.lineColor = getActiveLineColor();
addEntities([activeCircle], true); activeCircle.lineWidth = getActiveLineWidth();
activeCircle.lineDash = getActiveLineDash();
addEntities([activeCircle], true);
setGhostHelperEntities([]); setGhostHelperEntities([]);
return { return {
centerPoint: null, centerPoint: null,
}; };
}), }),
}, },
}, }
); );
@@ -1,171 +1,169 @@
import type {Point} from '@flatten-js/core'; import type { Point } from '@flatten-js/core';
import {LineEntity} from '../entities/LineEntity'; import { LineEntity } from '../entities/LineEntity';
import { import {
addEntities, addEntities,
getActiveLayerId, getActiveLayerId,
getActiveLineColor, getActiveLineColor,
getActiveLineWidth, getActiveLineDash,
setActiveToolActor, getActiveLineWidth,
setAngleGuideOriginPoint, setActiveToolActor,
setGhostHelperEntities, setAngleGuideOriginPoint,
setSelectedEntityIds, setGhostHelperEntities,
setShouldDrawHelpers, setSelectedEntityIds,
setShouldDrawHelpers,
} from '../state'; } from '../state';
import {Tool} from '../tools'; import { Tool } from '../tools';
import {Actor, assign, createMachine} from 'xstate'; import { Actor, assign, createMachine } from 'xstate';
import type {DrawEvent, PointInputEvent, StateEvent, ToolContext,} from './tool.types'; import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types';
import {selectToolStateMachine} from './select-tool.ts'; import { selectToolStateMachine } from './select-tool.ts';
import {getPointFromEvent} from '../helpers/get-point-from-event.ts'; import { getPointFromEvent } from '../helpers/get-point-from-event.ts';
export interface LineContext extends ToolContext { export interface LineContext extends ToolContext {
startPoint: Point | null; startPoint: Point | null;
} }
export enum LineState { export enum LineState {
INIT = 'INIT', INIT = 'INIT',
WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT', WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT',
WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT', WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT',
} }
export enum LineAction { export enum LineAction {
INIT_LINE_TOOL = 'INIT_LINE_TOOL', INIT_LINE_TOOL = 'INIT_LINE_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT', RECORD_START_POINT = 'RECORD_START_POINT',
DRAW_TEMP_LINE = 'DRAW_TEMP_LINE', DRAW_TEMP_LINE = 'DRAW_TEMP_LINE',
DRAW_FINAL_LINE = 'DRAW_FINAL_LINE', DRAW_FINAL_LINE = 'DRAW_FINAL_LINE',
SWITCH_TO_SELECT_TOOL = 'SWITCH_TO_SELECT_TOOL', SWITCH_TO_SELECT_TOOL = 'SWITCH_TO_SELECT_TOOL',
} }
export const lineToolStateMachine = createMachine( export const lineToolStateMachine = createMachine(
{ {
types: {} as { types: {} as {
context: LineContext; context: LineContext;
events: StateEvent; events: StateEvent;
}, },
context: { context: {
startPoint: null, startPoint: null,
type: Tool.LINE, type: Tool.LINE,
}, },
initial: LineState.INIT, initial: LineState.INIT,
states: { states: {
[LineState.INIT]: { [LineState.INIT]: {
description: 'Initializing the line tool', description: 'Initializing the line tool',
always: { always: {
actions: LineAction.INIT_LINE_TOOL, actions: LineAction.INIT_LINE_TOOL,
target: LineState.WAITING_FOR_START_POINT, target: LineState.WAITING_FOR_START_POINT,
}, },
}, },
[LineState.WAITING_FOR_START_POINT]: { [LineState.WAITING_FOR_START_POINT]: {
description: 'Select the start point of the line', description: 'Select the start point of the line',
meta: { meta: {
instructions: 'Select the start point of the line', instructions: 'Select the start point of the line',
}, },
on: { on: {
MOUSE_CLICK: { MOUSE_CLICK: {
actions: LineAction.RECORD_START_POINT, actions: LineAction.RECORD_START_POINT,
target: LineState.WAITING_FOR_END_POINT, target: LineState.WAITING_FOR_END_POINT,
}, },
ABSOLUTE_POINT_INPUT: { ABSOLUTE_POINT_INPUT: {
actions: LineAction.RECORD_START_POINT, actions: LineAction.RECORD_START_POINT,
target: LineState.WAITING_FOR_END_POINT, target: LineState.WAITING_FOR_END_POINT,
}, },
ESC: { ESC: {
actions: LineAction.SWITCH_TO_SELECT_TOOL, actions: LineAction.SWITCH_TO_SELECT_TOOL,
}, },
}, },
}, },
[LineState.WAITING_FOR_END_POINT]: { [LineState.WAITING_FOR_END_POINT]: {
description: 'Select the end point of the line', description: 'Select the end point of the line',
meta: { meta: {
instructions: 'Select the end point of the line', instructions: 'Select the end point of the line',
}, },
on: { on: {
DRAW: { DRAW: {
actions: LineAction.DRAW_TEMP_LINE, actions: LineAction.DRAW_TEMP_LINE,
}, },
MOUSE_CLICK: { MOUSE_CLICK: {
actions: LineAction.DRAW_FINAL_LINE, actions: LineAction.DRAW_FINAL_LINE,
target: LineState.WAITING_FOR_END_POINT, target: LineState.WAITING_FOR_END_POINT,
}, },
NUMBER_INPUT: { NUMBER_INPUT: {
actions: LineAction.DRAW_FINAL_LINE, actions: LineAction.DRAW_FINAL_LINE,
target: LineState.WAITING_FOR_END_POINT, target: LineState.WAITING_FOR_END_POINT,
}, },
ABSOLUTE_POINT_INPUT: { ABSOLUTE_POINT_INPUT: {
actions: LineAction.DRAW_FINAL_LINE, actions: LineAction.DRAW_FINAL_LINE,
target: LineState.WAITING_FOR_END_POINT, target: LineState.WAITING_FOR_END_POINT,
}, },
RELATIVE_POINT_INPUT: { RELATIVE_POINT_INPUT: {
actions: LineAction.DRAW_FINAL_LINE, actions: LineAction.DRAW_FINAL_LINE,
target: LineState.WAITING_FOR_END_POINT, target: LineState.WAITING_FOR_END_POINT,
}, },
ESC: { ESC: {
target: LineState.INIT, target: LineState.INIT,
}, },
ENTER: { ENTER: {
target: LineState.INIT, target: LineState.INIT,
}, },
}, },
}, },
}, },
}, },
{ {
actions: { actions: {
[LineAction.INIT_LINE_TOOL]: assign(() => { [LineAction.INIT_LINE_TOOL]: assign(() => {
setShouldDrawHelpers(true); setShouldDrawHelpers(true);
setSelectedEntityIds([]); setSelectedEntityIds([]);
setGhostHelperEntities([]); setGhostHelperEntities([]);
setAngleGuideOriginPoint(null); setAngleGuideOriginPoint(null);
return { return {
startPoint: null, startPoint: null,
}; };
}), }),
[LineAction.RECORD_START_POINT]: assign(({ event }) => { [LineAction.RECORD_START_POINT]: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent); const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint); setAngleGuideOriginPoint(startPoint);
return { return {
startPoint, startPoint,
}; };
}), }),
[LineAction.DRAW_TEMP_LINE]: ({ context, event }) => { [LineAction.DRAW_TEMP_LINE]: ({ context, event }) => {
const activeLine = new LineEntity( const activeLine = new LineEntity(
getActiveLayerId(), getActiveLayerId(),
context.startPoint as Point, context.startPoint as Point,
(event as DrawEvent).drawController.getWorldMouseLocation(), (event as DrawEvent).drawController.getWorldMouseLocation()
); );
activeLine.lineColor = getActiveLineColor(); activeLine.lineColor = getActiveLineColor();
activeLine.lineWidth = getActiveLineWidth(); activeLine.lineWidth = getActiveLineWidth();
setGhostHelperEntities([activeLine]); activeLine.lineDash = getActiveLineDash();
}, setGhostHelperEntities([activeLine]);
[LineAction.DRAW_FINAL_LINE]: assign(({ context, event }) => { },
if (!context.startPoint) { [LineAction.DRAW_FINAL_LINE]: assign(({ context, event }) => {
throw new Error( if (!context.startPoint) {
'Start point is not set during DRAW_FINAL_LINE in LineEntity', throw new Error('Start point is not set during DRAW_FINAL_LINE in LineEntity');
); }
}
const endPoint = getPointFromEvent( const endPoint = getPointFromEvent(context.startPoint, event as PointInputEvent);
context.startPoint, const activeLine = new LineEntity(
event as PointInputEvent, getActiveLayerId(),
); context.startPoint as Point,
const activeLine = new LineEntity( endPoint
getActiveLayerId(), );
context.startPoint as Point, activeLine.lineColor = getActiveLineColor();
endPoint, activeLine.lineWidth = getActiveLineWidth();
); activeLine.lineDash = getActiveLineDash();
activeLine.lineColor = getActiveLineColor(); addEntities([activeLine], true);
activeLine.lineWidth = getActiveLineWidth();
addEntities([activeLine], true);
// Keep drawing from the last point // Keep drawing from the last point
setGhostHelperEntities([new LineEntity(getActiveLayerId(), endPoint, endPoint)]); setGhostHelperEntities([new LineEntity(getActiveLayerId(), endPoint, endPoint)]);
setAngleGuideOriginPoint(endPoint); setAngleGuideOriginPoint(endPoint);
return { return {
startPoint: endPoint, startPoint: endPoint,
}; };
}), }),
[LineAction.SWITCH_TO_SELECT_TOOL]: () => { [LineAction.SWITCH_TO_SELECT_TOOL]: () => {
setActiveToolActor(new Actor(selectToolStateMachine)); setActiveToolActor(new Actor(selectToolStateMachine));
}, },
}, },
}, }
); );
@@ -1,226 +1,221 @@
import {type Point, Vector} from '@flatten-js/core'; import { type Point, Vector } from '@flatten-js/core';
import {MeasurementEntity} from '../entities/MeasurementEntity'; import { MeasurementEntity } from '../entities/MeasurementEntity';
import { import {
addEntities, addEntities,
getActiveLayerId, getActiveLayerId,
getActiveLineColor, getActiveLineColor,
getActiveLineWidth, getActiveLineDash,
setAngleGuideOriginPoint, getActiveLineWidth,
setGhostHelperEntities, setAngleGuideOriginPoint,
setSelectedEntityIds, setGhostHelperEntities,
setShouldDrawHelpers, setSelectedEntityIds,
setShouldDrawHelpers,
} from '../state'; } from '../state';
import {Tool} from '../tools'; import { Tool } from '../tools';
import {assign, createMachine} from 'xstate'; import { assign, createMachine } from 'xstate';
import type {DrawEvent, PointInputEvent, StateEvent, ToolContext,} from './tool.types'; import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types';
import {MEASUREMENT_DEFAULT_OFFSET, TO_RADIANS} from '../App.consts'; import { MEASUREMENT_DEFAULT_OFFSET, TO_RADIANS } from '../App.consts';
import {getPointFromEvent} from '../helpers/get-point-from-event.ts'; import { getPointFromEvent } from '../helpers/get-point-from-event.ts';
import {isPointEqual} from '../helpers/is-point-equal.ts'; import { isPointEqual } from '../helpers/is-point-equal.ts';
export interface MeasurementContext extends ToolContext { export interface MeasurementContext extends ToolContext {
startPoint: Point | null; startPoint: Point | null;
endPoint: Point | null; endPoint: Point | null;
} }
export enum MeasurementState { export enum MeasurementState {
INIT = 'INIT', INIT = 'INIT',
WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT', WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT',
WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT', WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT',
WAITING_FOR_OFFSET = 'WAITING_FOR_OFFSET', WAITING_FOR_OFFSET = 'WAITING_FOR_OFFSET',
} }
export enum MeasurementAction { export enum MeasurementAction {
INIT_MEASUREMENT_TOOL = 'INIT_MEASUREMENT_TOOL', INIT_MEASUREMENT_TOOL = 'INIT_MEASUREMENT_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT', RECORD_START_POINT = 'RECORD_START_POINT',
RECORD_END_POINT = 'RECORD_END_POINT', RECORD_END_POINT = 'RECORD_END_POINT',
DRAW_TEMP_MEASUREMENT = 'DRAW_TEMP_MEASUREMENT', DRAW_TEMP_MEASUREMENT = 'DRAW_TEMP_MEASUREMENT',
DRAW_FINAL_MEASUREMENT = 'DRAW_FINAL_MEASUREMENT', DRAW_FINAL_MEASUREMENT = 'DRAW_FINAL_MEASUREMENT',
} }
export const measurementToolStateMachine = createMachine( export const measurementToolStateMachine = createMachine(
{ {
types: {} as { types: {} as {
context: MeasurementContext; context: MeasurementContext;
events: StateEvent; events: StateEvent;
}, },
context: { context: {
startPoint: null, startPoint: null,
endPoint: null, endPoint: null,
type: Tool.MEASUREMENT, type: Tool.MEASUREMENT,
}, },
initial: MeasurementState.INIT, initial: MeasurementState.INIT,
states: { states: {
[MeasurementState.INIT]: { [MeasurementState.INIT]: {
description: 'Initializing the line tool', description: 'Initializing the line tool',
always: { always: {
actions: MeasurementAction.INIT_MEASUREMENT_TOOL, actions: MeasurementAction.INIT_MEASUREMENT_TOOL,
target: MeasurementState.WAITING_FOR_START_POINT, target: MeasurementState.WAITING_FOR_START_POINT,
}, },
}, },
[MeasurementState.WAITING_FOR_START_POINT]: { [MeasurementState.WAITING_FOR_START_POINT]: {
description: 'Select the start point of the measurement', description: 'Select the start point of the measurement',
meta: { meta: {
instructions: 'Select the start point of the measurement', instructions: 'Select the start point of the measurement',
}, },
on: { on: {
MOUSE_CLICK: { MOUSE_CLICK: {
actions: MeasurementAction.RECORD_START_POINT, actions: MeasurementAction.RECORD_START_POINT,
target: MeasurementState.WAITING_FOR_END_POINT, target: MeasurementState.WAITING_FOR_END_POINT,
}, },
ABSOLUTE_POINT_INPUT: { ABSOLUTE_POINT_INPUT: {
actions: MeasurementAction.RECORD_START_POINT, actions: MeasurementAction.RECORD_START_POINT,
target: MeasurementState.WAITING_FOR_END_POINT, target: MeasurementState.WAITING_FOR_END_POINT,
}, },
}, },
}, },
[MeasurementState.WAITING_FOR_END_POINT]: { [MeasurementState.WAITING_FOR_END_POINT]: {
description: 'Select the end point of the measurement', description: 'Select the end point of the measurement',
meta: { meta: {
instructions: 'Select the end point of the measurement', instructions: 'Select the end point of the measurement',
}, },
on: { on: {
DRAW: { DRAW: {
actions: MeasurementAction.DRAW_TEMP_MEASUREMENT, actions: MeasurementAction.DRAW_TEMP_MEASUREMENT,
}, },
MOUSE_CLICK: { MOUSE_CLICK: {
actions: MeasurementAction.RECORD_END_POINT, actions: MeasurementAction.RECORD_END_POINT,
target: MeasurementState.WAITING_FOR_OFFSET, target: MeasurementState.WAITING_FOR_OFFSET,
}, },
NUMBER_INPUT: { NUMBER_INPUT: {
actions: MeasurementAction.RECORD_END_POINT, actions: MeasurementAction.RECORD_END_POINT,
target: MeasurementState.WAITING_FOR_OFFSET, target: MeasurementState.WAITING_FOR_OFFSET,
}, },
ABSOLUTE_POINT_INPUT: { ABSOLUTE_POINT_INPUT: {
actions: MeasurementAction.RECORD_END_POINT, actions: MeasurementAction.RECORD_END_POINT,
target: MeasurementState.WAITING_FOR_OFFSET, target: MeasurementState.WAITING_FOR_OFFSET,
}, },
RELATIVE_POINT_INPUT: { RELATIVE_POINT_INPUT: {
actions: MeasurementAction.RECORD_END_POINT, actions: MeasurementAction.RECORD_END_POINT,
target: MeasurementState.WAITING_FOR_OFFSET, target: MeasurementState.WAITING_FOR_OFFSET,
}, },
ESC: { ESC: {
target: MeasurementState.INIT, target: MeasurementState.INIT,
}, },
}, },
}, },
[MeasurementState.WAITING_FOR_OFFSET]: { [MeasurementState.WAITING_FOR_OFFSET]: {
description: 'Select the offset to display the measurement at', description: 'Select the offset to display the measurement at',
meta: { meta: {
instructions: 'Select the offset to display the measurement at', instructions: 'Select the offset to display the measurement at',
}, },
on: { on: {
DRAW: { DRAW: {
actions: MeasurementAction.DRAW_TEMP_MEASUREMENT, actions: MeasurementAction.DRAW_TEMP_MEASUREMENT,
}, },
MOUSE_CLICK: { MOUSE_CLICK: {
actions: MeasurementAction.DRAW_FINAL_MEASUREMENT, actions: MeasurementAction.DRAW_FINAL_MEASUREMENT,
target: MeasurementState.INIT, target: MeasurementState.INIT,
}, },
NUMBER_INPUT: { NUMBER_INPUT: {
actions: MeasurementAction.DRAW_FINAL_MEASUREMENT, actions: MeasurementAction.DRAW_FINAL_MEASUREMENT,
target: MeasurementState.INIT, target: MeasurementState.INIT,
}, },
ABSOLUTE_POINT_INPUT: { ABSOLUTE_POINT_INPUT: {
actions: MeasurementAction.DRAW_FINAL_MEASUREMENT, actions: MeasurementAction.DRAW_FINAL_MEASUREMENT,
target: MeasurementState.INIT, target: MeasurementState.INIT,
}, },
RELATIVE_POINT_INPUT: { RELATIVE_POINT_INPUT: {
actions: MeasurementAction.DRAW_FINAL_MEASUREMENT, actions: MeasurementAction.DRAW_FINAL_MEASUREMENT,
target: MeasurementState.INIT, target: MeasurementState.INIT,
}, },
ESC: { ESC: {
target: MeasurementState.INIT, target: MeasurementState.INIT,
}, },
}, },
}, },
}, },
}, },
{ {
actions: { actions: {
[MeasurementAction.INIT_MEASUREMENT_TOOL]: assign(() => { [MeasurementAction.INIT_MEASUREMENT_TOOL]: assign(() => {
setShouldDrawHelpers(true); setShouldDrawHelpers(true);
setSelectedEntityIds([]); setSelectedEntityIds([]);
setGhostHelperEntities([]); setGhostHelperEntities([]);
setAngleGuideOriginPoint(null); setAngleGuideOriginPoint(null);
return { return {
startPoint: null, startPoint: null,
endPoint: null, endPoint: null,
}; };
}), }),
[MeasurementAction.RECORD_START_POINT]: assign(({ event }) => { [MeasurementAction.RECORD_START_POINT]: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent); const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint); setAngleGuideOriginPoint(startPoint);
return { return {
startPoint, startPoint,
}; };
}), }),
[MeasurementAction.RECORD_END_POINT]: assign(({ context, event }) => { [MeasurementAction.RECORD_END_POINT]: assign(({ context, event }) => {
const endPoint = getPointFromEvent( const endPoint = getPointFromEvent(context.startPoint, event as PointInputEvent);
context.startPoint, setAngleGuideOriginPoint(endPoint);
event as PointInputEvent, return {
); ...context,
setAngleGuideOriginPoint(endPoint); endPoint,
return { };
...context, }),
endPoint, [MeasurementAction.DRAW_TEMP_MEASUREMENT]: ({ context, event }) => {
}; const startPoint = context.startPoint as Point;
}),
[MeasurementAction.DRAW_TEMP_MEASUREMENT]: ({ context, event }) => {
const startPoint = context.startPoint as Point;
let endPoint: Point; let endPoint: Point;
let offsetPoint: Point; let offsetPoint: Point;
if (!context.endPoint) { if (!context.endPoint) {
// User has drawn startPoint, but not yet endPoint // User has drawn startPoint, but not yet endPoint
// Endpoint should be the mouse location and offset should be MEASUREMENT_DEFAULT_OFFSET to either direction // Endpoint should be the mouse location and offset should be MEASUREMENT_DEFAULT_OFFSET to either direction
endPoint = ( endPoint = (event as DrawEvent).drawController.getWorldMouseLocation();
event as DrawEvent
).drawController.getWorldMouseLocation();
if (isPointEqual(startPoint, endPoint)) { if (isPointEqual(startPoint, endPoint)) {
return; // Cannot draw temp measurement when start and endpoint are equal return; // Cannot draw temp measurement when start and endpoint are equal
} }
const normalVector = new Vector(startPoint, endPoint) const normalVector = new Vector(startPoint, endPoint)
.rotate(-90 * TO_RADIANS) .rotate(-90 * TO_RADIANS)
.normalize(); .normalize();
offsetPoint = startPoint // Pixel constant → world units so the default offset is zoom-independent
.clone() const worldFactor = (event as DrawEvent).drawController.getScreenScale() || 1;
.translate(normalVector.multiply(MEASUREMENT_DEFAULT_OFFSET)); offsetPoint = startPoint
} else { .clone()
// User has already selected a startPoint and endPoint .translate(normalVector.multiply(MEASUREMENT_DEFAULT_OFFSET / worldFactor));
// The offsetPoint should be set to the mouse location } else {
endPoint = context.endPoint as Point; // User has already selected a startPoint and endPoint
offsetPoint = ( // The offsetPoint should be set to the mouse location
event as DrawEvent endPoint = context.endPoint as Point;
).drawController.getWorldMouseLocation(); offsetPoint = (event as DrawEvent).drawController.getWorldMouseLocation();
} }
const activeMeasurement = new MeasurementEntity( const activeMeasurement = new MeasurementEntity(
getActiveLayerId(), getActiveLayerId(),
context.startPoint as Point, context.startPoint as Point,
endPoint, endPoint,
offsetPoint, offsetPoint
); );
activeMeasurement.lineColor = getActiveLineColor(); activeMeasurement.lineColor = getActiveLineColor();
activeMeasurement.lineWidth = getActiveLineWidth(); activeMeasurement.lineWidth = getActiveLineWidth();
setGhostHelperEntities([activeMeasurement]); activeMeasurement.lineDash = getActiveLineDash();
}, setGhostHelperEntities([activeMeasurement]);
[MeasurementAction.DRAW_FINAL_MEASUREMENT]: ({ context, event }) => { },
const offsetPoint = getPointFromEvent( [MeasurementAction.DRAW_FINAL_MEASUREMENT]: ({ context, event }) => {
context.endPoint, const offsetPoint = getPointFromEvent(context.endPoint, event as PointInputEvent);
event as PointInputEvent, const activeMeasurement = new MeasurementEntity(
); getActiveLayerId(),
const activeMeasurement = new MeasurementEntity( context.startPoint as Point,
getActiveLayerId(), context.endPoint as Point,
context.startPoint as Point, offsetPoint
context.endPoint as Point, );
offsetPoint, activeMeasurement.lineColor = getActiveLineColor();
); activeMeasurement.lineWidth = getActiveLineWidth();
activeMeasurement.lineColor = getActiveLineColor(); activeMeasurement.lineDash = getActiveLineDash();
activeMeasurement.lineWidth = getActiveLineWidth(); addEntities([activeMeasurement], true);
addEntities([activeMeasurement], true); },
}, },
}, }
},
); );
@@ -1,154 +1,152 @@
import type {Point} from '@flatten-js/core'; import type { Point } from '@flatten-js/core';
import {RectangleEntity} from '../entities/RectangleEntity'; import { RectangleEntity } from '../entities/RectangleEntity';
import { import {
addEntities, addEntities,
getActiveLayerId, getActiveLayerId,
getActiveLineColor, getActiveLineColor,
getActiveLineWidth, getActiveLineDash,
setAngleGuideOriginPoint, getActiveLineWidth,
setGhostHelperEntities, setAngleGuideOriginPoint,
setSelectedEntityIds, setGhostHelperEntities,
setShouldDrawHelpers, setSelectedEntityIds,
setShouldDrawHelpers,
} from '../state'; } from '../state';
import type {DrawEvent, PointInputEvent, StateEvent, ToolContext,} from './tool.types'; import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types';
import {Tool} from '../tools'; import { Tool } from '../tools';
import {assign, createMachine} from 'xstate'; import { assign, createMachine } from 'xstate';
import {getPointFromEvent} from '../helpers/get-point-from-event.ts'; import { getPointFromEvent } from '../helpers/get-point-from-event.ts';
export interface RectangleContext extends ToolContext { export interface RectangleContext extends ToolContext {
startPoint: Point | null; startPoint: Point | null;
} }
export enum RectangleState { export enum RectangleState {
INIT = 'INIT', INIT = 'INIT',
WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT', WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT',
WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT', WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT',
} }
export enum RectangleAction { export enum RectangleAction {
INIT_RECTANGLE_TOOL = 'INIT_RECTANGLE_TOOL', INIT_RECTANGLE_TOOL = 'INIT_RECTANGLE_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT', RECORD_START_POINT = 'RECORD_START_POINT',
DRAW_TEMP_RECTANGLE = 'DRAW_TEMP_RECTANGLE', DRAW_TEMP_RECTANGLE = 'DRAW_TEMP_RECTANGLE',
DRAW_FINAL_RECTANGLE = 'DRAW_FINAL_RECTANGLE', DRAW_FINAL_RECTANGLE = 'DRAW_FINAL_RECTANGLE',
} }
export const rectangleToolStateMachine = createMachine( export const rectangleToolStateMachine = createMachine(
{ {
types: {} as { types: {} as {
context: RectangleContext; context: RectangleContext;
events: StateEvent; events: StateEvent;
}, },
context: { context: {
startPoint: null, startPoint: null,
type: Tool.RECTANGLE, type: Tool.RECTANGLE,
}, },
initial: RectangleState.INIT, initial: RectangleState.INIT,
states: { states: {
[RectangleState.INIT]: { [RectangleState.INIT]: {
description: 'Initializing the rectangle tool', description: 'Initializing the rectangle tool',
always: { always: {
actions: RectangleAction.INIT_RECTANGLE_TOOL, actions: RectangleAction.INIT_RECTANGLE_TOOL,
target: RectangleState.WAITING_FOR_START_POINT, target: RectangleState.WAITING_FOR_START_POINT,
}, },
}, },
[RectangleState.WAITING_FOR_START_POINT]: { [RectangleState.WAITING_FOR_START_POINT]: {
description: 'Select the start point of the rectangle', description: 'Select the start point of the rectangle',
meta: { meta: {
instructions: 'Select the start point of the rectangle', instructions: 'Select the start point of the rectangle',
}, },
on: { on: {
MOUSE_CLICK: { MOUSE_CLICK: {
actions: RectangleAction.RECORD_START_POINT, actions: RectangleAction.RECORD_START_POINT,
target: RectangleState.WAITING_FOR_END_POINT, target: RectangleState.WAITING_FOR_END_POINT,
}, },
ABSOLUTE_POINT_INPUT: { ABSOLUTE_POINT_INPUT: {
actions: RectangleAction.RECORD_START_POINT, actions: RectangleAction.RECORD_START_POINT,
target: RectangleState.WAITING_FOR_END_POINT, target: RectangleState.WAITING_FOR_END_POINT,
}, },
}, },
}, },
[RectangleState.WAITING_FOR_END_POINT]: { [RectangleState.WAITING_FOR_END_POINT]: {
description: 'Select the end point of the rectangle', description: 'Select the end point of the rectangle',
meta: { meta: {
instructions: 'Select the end point of the rectangle', instructions: 'Select the end point of the rectangle',
}, },
on: { on: {
DRAW: { DRAW: {
actions: RectangleAction.DRAW_TEMP_RECTANGLE, actions: RectangleAction.DRAW_TEMP_RECTANGLE,
}, },
MOUSE_CLICK: { MOUSE_CLICK: {
actions: RectangleAction.DRAW_FINAL_RECTANGLE, actions: RectangleAction.DRAW_FINAL_RECTANGLE,
target: RectangleState.INIT, target: RectangleState.INIT,
}, },
NUMBER_INPUT: { NUMBER_INPUT: {
actions: RectangleAction.DRAW_FINAL_RECTANGLE, // TODO see if we want to add a flow where you enter the width and then the height if one of the dimensions of the "direction + distance" comes out to 0 actions: RectangleAction.DRAW_FINAL_RECTANGLE, // TODO see if we want to add a flow where you enter the width and then the height if one of the dimensions of the "direction + distance" comes out to 0
target: RectangleState.INIT, target: RectangleState.INIT,
}, },
ABSOLUTE_POINT_INPUT: { ABSOLUTE_POINT_INPUT: {
actions: RectangleAction.DRAW_FINAL_RECTANGLE, actions: RectangleAction.DRAW_FINAL_RECTANGLE,
target: RectangleState.INIT, target: RectangleState.INIT,
}, },
RELATIVE_POINT_INPUT: { RELATIVE_POINT_INPUT: {
actions: RectangleAction.DRAW_FINAL_RECTANGLE, actions: RectangleAction.DRAW_FINAL_RECTANGLE,
target: RectangleState.INIT, target: RectangleState.INIT,
}, },
ESC: { ESC: {
target: RectangleState.INIT, target: RectangleState.INIT,
}, },
}, },
}, },
}, },
}, },
{ {
actions: { actions: {
[RectangleAction.INIT_RECTANGLE_TOOL]: () => { [RectangleAction.INIT_RECTANGLE_TOOL]: () => {
setShouldDrawHelpers(true); setShouldDrawHelpers(true);
setGhostHelperEntities([]); setGhostHelperEntities([]);
setSelectedEntityIds([]); setSelectedEntityIds([]);
setAngleGuideOriginPoint(null); setAngleGuideOriginPoint(null);
}, },
[RectangleAction.RECORD_START_POINT]: assign(({ event }) => { [RectangleAction.RECORD_START_POINT]: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent); const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint); setAngleGuideOriginPoint(startPoint);
return { return {
startPoint, startPoint,
}; };
}), }),
[RectangleAction.DRAW_TEMP_RECTANGLE]: ({ context, event }) => { [RectangleAction.DRAW_TEMP_RECTANGLE]: ({ context, event }) => {
if (!context.startPoint) { if (!context.startPoint) {
throw new Error( throw new Error('[RECTANGLE]: calling draw without start point being set');
'[RECTANGLE]: calling draw without start point being set', }
); const activeRectangle = new RectangleEntity(
} getActiveLayerId(),
const activeRectangle = new RectangleEntity( context.startPoint as Point,
getActiveLayerId(), (event as DrawEvent).drawController.getWorldMouseLocation()
context.startPoint as Point, );
(event as DrawEvent).drawController.getWorldMouseLocation(), activeRectangle.lineColor = getActiveLineColor();
); activeRectangle.lineWidth = getActiveLineWidth();
activeRectangle.lineColor = getActiveLineColor(); activeRectangle.lineDash = getActiveLineDash();
activeRectangle.lineWidth = getActiveLineWidth(); setGhostHelperEntities([activeRectangle]);
setGhostHelperEntities([activeRectangle]); },
}, [RectangleAction.DRAW_FINAL_RECTANGLE]: ({ context, event }) => {
[RectangleAction.DRAW_FINAL_RECTANGLE]: ({ context, event }) => { if (!context.startPoint) {
if (!context.startPoint) { throw Error(
throw Error( 'Trying to DRAW_FINAL_RECTANGLE when startPoint is not defined in rectangle-tool'
'Trying to DRAW_FINAL_RECTANGLE when startPoint is not defined in rectangle-tool', );
); }
} const endPoint = getPointFromEvent(context.startPoint, event as PointInputEvent);
const endPoint = getPointFromEvent(
context.startPoint,
event as PointInputEvent,
);
const activeRectangle = new RectangleEntity( const activeRectangle = new RectangleEntity(
getActiveLayerId(), getActiveLayerId(),
context.startPoint as Point, context.startPoint as Point,
endPoint, endPoint
); );
activeRectangle.lineColor = getActiveLineColor(); activeRectangle.lineColor = getActiveLineColor();
activeRectangle.lineWidth = getActiveLineWidth(); activeRectangle.lineWidth = getActiveLineWidth();
addEntities([activeRectangle], true); activeRectangle.lineDash = getActiveLineDash();
}, addEntities([activeRectangle], true);
}, },
}, },
}
); );