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]
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]:
"""횡단면 상세에서 DB data 컬럼에 저장할 요약을 만든다."""
samples = cross_section.get("samples", [])
@@ -101,8 +125,7 @@ def run_section_generation(
cross_records: list[dict[str, Any]] = []
for seq, cross_section in enumerate(result["cross_sections"]):
chainage = float(cross_section["chainage_m"])
filename = f"cross_{int(round(chainage)):05d}m.json"
cross_file = cross_dir / filename
cross_file = cross_dir / cross_filename(chainage)
atomic_write_json(cross_file, cross_section)
cross_records.append(
{
@@ -12,7 +12,10 @@ from fastapi import APIRouter
from fastapi.responses import JSONResponse
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 B06_wf3_ProfileCross.B06_wf3_ProfileCross_Repository import (
confirm_sections_for_route,
@@ -122,9 +125,12 @@ def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dic
raise FileNotFoundError("횡단면 상세 파일을 찾을 수 없습니다.")
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 = [
json.loads(path.read_text(encoding="utf-8"))
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(
isinstance(section, dict) for section in cross_sections
@@ -12,6 +12,7 @@ from fastapi import APIRouter
from fastapi.responses import JSONResponse
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 (
get_confirmed_route_context,
get_longitudinal_section,
@@ -63,11 +64,16 @@ def _read_json(path: Path) -> dict[str, Any]:
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"
if not cross_dir.is_dir():
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]]:
@@ -114,7 +120,7 @@ def _drawing_list(project_root: Path, longitudinal_path: Path) -> list[DesignDra
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)
if not match:
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
* 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
* 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
* Screen pixels (zoom-independent)
*/
export const MEASUREMENT_DEFAULT_OFFSET = 200;
export const MEASUREMENT_DEFAULT_OFFSET = 60;
/**
* 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
* 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
* Screen pixels (zoom-independent)
*/
export const MEASUREMENT_FONT_SIZE = 40;
export const MEASUREMENT_FONT_SIZE = 16;
/**
* Colors for the selection rectangle
@@ -148,6 +148,45 @@ body > canvas[data-id="canvas"] {
font-size: 10px;
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 {
display: flex;
flex-direction: column;
@@ -4,10 +4,18 @@ import { Actor } from 'xstate';
import { HtmlEvent, type Layer } from '../App.types';
import { exportEntitiesToJsonFile } from '../helpers/import-export-handlers/export-entities-to-json';
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 {
getActiveLayerId,
getActiveLineColor,
getActiveLineDash,
getActiveLineWidth,
getActiveTextStyle,
getActiveToolActor,
getAngleStep,
getEntities,
getGridEnabled,
getInputController,
getLastStateInstructions,
@@ -17,8 +25,13 @@ import {
getSnapEnabled,
redo,
setActiveLayerId,
setActiveLineColor,
setActiveLineDash,
setActiveLineWidth,
setActiveTextStyle,
setActiveToolActor,
setAngleStep,
setEntities,
setGridEnabled,
setLayers,
setSnapEnabled,
@@ -69,6 +82,20 @@ const RIBBON_GROUPS: { label: string; tools: RibbonTool[] }[] = [
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 = () => {
const [activeTool, setActiveTool] = useState<Tool>(Tool.LINE);
const [zoom, setZoom] = useState(1);
@@ -84,6 +111,10 @@ export const Toolbar: FC = () => {
const [ortho, setOrtho] = useState(getAngleStep() === 90);
const [command, setCommand] = 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(() => {
setActiveTool(getActiveToolActor()?.getSnapshot()?.context.type ?? Tool.LINE);
@@ -99,6 +130,10 @@ export const Toolbar: FC = () => {
setSnap(getSnapEnabled());
setGrid(getGridEnabled());
setOrtho(getAngleStep() === 90);
setLineColorLocal(getActiveLineColor());
setLineWidthLocal(getActiveLineWidth());
setLineTypeLocal(dashToLineType(getActiveLineDash()));
setTextStyleLocal({ ...getActiveTextStyle() });
}, []);
useEffect(() => {
@@ -136,6 +171,55 @@ export const Toolbar: FC = () => {
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 (
<>
<header className="cad-titlebar controls">
@@ -195,6 +279,83 @@ export const Toolbar: FC = () => {
<span className="cad-ribbon-group__label">{group.label}</span>
</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>
<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 {uniqWith} from 'es-toolkit';
import {type Shape, type SnapPoint, SnapPointType, type StartAndEndpointEntity} from '../App.types';
import type {DrawController} from '../drawControllers/DrawController.ts';
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 {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';
import { Arc, type Box, Line, Point, type Segment } from '@flatten-js/core';
import { uniqWith } from 'es-toolkit';
import {
type Shape,
type SnapPoint,
SnapPointType,
type StartAndEndpointEntity,
} from '../App.types';
import type { DrawController } from '../drawControllers/DrawController.ts';
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 { 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 {
public id: string = crypto.randomUUID();
@@ -211,6 +216,7 @@ export class ArcEntity implements Entity, StartAndEndpointEntity {
type: EntityName.Arc,
lineColor: this.lineColor,
lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId,
shapeData: {
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.lineColor = jsonEntity.lineColor;
arcEntity.lineWidth = jsonEntity.lineWidth;
arcEntity.lineDash = jsonEntity.lineDash;
return arcEntity;
}
@@ -1,12 +1,12 @@
import {Box, Point, Segment} from '@flatten-js/core';
import {max, min} from 'es-toolkit/compat';
import type {Shape, SnapPoint} from '../App.types';
import type {DrawController} from '../drawControllers/DrawController';
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts';
import {scalePoint} from '../helpers/scale-point';
import {getActiveLayerId} from '../state.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity';
import type {LineEntity} from './LineEntity.ts';
import { Box, Point, Segment } from '@flatten-js/core';
import { max, min } from 'es-toolkit/compat';
import type { Shape, SnapPoint } from '../App.types';
import type { DrawController } from '../drawControllers/DrawController';
import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import { scalePoint } from '../helpers/scale-point';
import { getActiveLayerId } from '../state.ts';
import { type Entity, EntityName, type JsonEntity } from './Entity';
import type { LineEntity } from './LineEntity.ts';
export class ArrowHeadEntity implements Entity {
public id: string = crypto.randomUUID();
@@ -138,6 +138,7 @@ export class ArrowHeadEntity implements Entity {
type: EntityName.ArrowHead,
lineColor: this.lineColor,
lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId,
shapeData: {
p1: { x: this.p1.x, y: this.p1.y },
@@ -160,6 +161,7 @@ export class ArrowHeadEntity implements Entity {
lineEntity.id = jsonEntity.id;
lineEntity.lineColor = jsonEntity.lineColor;
lineEntity.lineWidth = jsonEntity.lineWidth;
lineEntity.lineDash = jsonEntity.lineDash ?? [];
return lineEntity;
}
}
@@ -1,12 +1,12 @@
import {type Box, Circle, Point, type Segment} from '@flatten-js/core';
import {type Shape, type SnapPoint, SnapPointType} from '../App.types';
import type {DrawController} from '../drawControllers/DrawController';
import {getExportColor} from '../helpers/get-export-color';
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';
import type {LineEntity} from './LineEntity.ts';
import { type Box, Circle, Point, type Segment } from '@flatten-js/core';
import { type Shape, type SnapPoint, SnapPointType } from '../App.types';
import type { DrawController } from '../drawControllers/DrawController';
import { getExportColor } from '../helpers/get-export-color';
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';
import type { LineEntity } from './LineEntity.ts';
export class CircleEntity implements Entity {
public id: string = crypto.randomUUID();
@@ -168,6 +168,7 @@ export class CircleEntity implements Entity {
type: EntityName.Circle,
lineColor: this.lineColor,
lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId,
shapeData: {
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.lineColor = jsonEntity.lineColor;
circleEntity.lineWidth = jsonEntity.lineWidth;
circleEntity.lineDash = jsonEntity.lineDash;
return circleEntity;
}
@@ -1,14 +1,14 @@
import type {Box, Point, Segment} from '@flatten-js/core';
import type {Shape, SnapPoint} from '../App.types';
import type {DrawController} from '../drawControllers/DrawController.ts';
import type {ArcJsonData} from './ArcEntity';
import type {ArrowHeadJsonData} from './ArrowHeadEntity.ts';
import type {CircleJsonData} from './CircleEntity';
import type {ImageJsonData} from './ImageEntity';
import type {LineEntity, LineJsonData} from './LineEntity';
import type {PointJsonData} from './PointEntity';
import type {RectangleJsonData} from './RectangleEntity';
import type {TextJsonData} from './TextEntity.ts';
import type { Box, Point, Segment } from '@flatten-js/core';
import type { Shape, SnapPoint } from '../App.types';
import type { DrawController } from '../drawControllers/DrawController.ts';
import type { ArcJsonData } from './ArcEntity';
import type { ArrowHeadJsonData } from './ArrowHeadEntity.ts';
import type { CircleJsonData } from './CircleEntity';
import type { ImageJsonData } from './ImageEntity';
import type { LineEntity, LineJsonData } from './LineEntity';
import type { PointJsonData } from './PointEntity';
import type { RectangleJsonData } from './RectangleEntity';
import type { TextJsonData } from './TextEntity.ts';
export interface Entity {
// Random uuid generated when the Entity is created
@@ -74,6 +74,7 @@ export interface JsonEntity<TShapeJsonData = ShapeJsonData> {
type: EntityName;
lineColor: string;
lineWidth: number;
lineDash?: number[];
layerId: string;
shapeData: TShapeJsonData | null;
children?: JsonEntity<ShapeJsonData>[];
@@ -1,16 +1,16 @@
import type * as Flatten 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 {DrawController} from '../drawControllers/DrawController.ts';
import {twoPointBoxToPolygon} from '../helpers/box-to-polygon';
import {getExportColor} from '../helpers/get-export-color';
import {mirrorAngleOverAxis} from '../helpers/mirror-angle-over-axis.ts';
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts';
import {polygonToSegments} from '../helpers/polygon-to-segments';
import {scalePoint} from '../helpers/scale-point';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity';
import type {LineEntity} from './LineEntity.ts';
import { type Box, Point, Polygon, Relations, type Segment, Vector } from '@flatten-js/core';
import { type Shape, type SnapPoint, SnapPointType } from '../App.types';
import type { DrawController } from '../drawControllers/DrawController.ts';
import { twoPointBoxToPolygon } from '../helpers/box-to-polygon';
import { getExportColor } from '../helpers/get-export-color';
import { mirrorAngleOverAxis } from '../helpers/mirror-angle-over-axis.ts';
import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import { polygonToSegments } from '../helpers/polygon-to-segments';
import { scalePoint } from '../helpers/scale-point';
import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts';
import { type Entity, EntityName, type JsonEntity } from './Entity';
import type { LineEntity } from './LineEntity.ts';
export class ImageEntity implements Entity {
public id: string = crypto.randomUUID();
@@ -213,6 +213,7 @@ export class ImageEntity implements Entity {
type: EntityName.Image,
lineColor: this.lineColor,
lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId,
shapeData: {
points: this.polygon.vertices.map((vertex) => ({
@@ -241,6 +242,7 @@ export class ImageEntity implements Entity {
rectangleEntity.id = jsonEntity.id;
rectangleEntity.lineColor = jsonEntity.lineColor;
rectangleEntity.lineWidth = jsonEntity.lineWidth;
rectangleEntity.lineDash = jsonEntity.lineDash;
return rectangleEntity;
}
}
@@ -1,15 +1,20 @@
import {type Box, Point, Segment} from '@flatten-js/core';
import {sortBy, uniqWith} from 'es-toolkit';
import {type Shape, type SnapPoint, SnapPointType, type StartAndEndpointEntity} from '../App.types';
import type {DrawController} from '../drawControllers/DrawController';
import {pointDistance} from '../helpers/distance-between-points';
import {getAngleWithXAxis} from '../helpers/get-angle-with-x-axis.ts';
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';
import { type Box, Point, Segment } from '@flatten-js/core';
import { sortBy, uniqWith } from 'es-toolkit';
import {
type Shape,
type SnapPoint,
SnapPointType,
type StartAndEndpointEntity,
} from '../App.types';
import type { DrawController } from '../drawControllers/DrawController';
import { pointDistance } from '../helpers/distance-between-points';
import { getAngleWithXAxis } from '../helpers/get-angle-with-x-axis.ts';
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 {
public id: string = crypto.randomUUID();
@@ -174,6 +179,7 @@ export class LineEntity implements Entity, StartAndEndpointEntity {
type: EntityName.Line,
lineColor: this.lineColor,
lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId,
shapeData: {
startPoint: {
@@ -202,6 +208,7 @@ export class LineEntity implements Entity, StartAndEndpointEntity {
lineEntity.id = jsonEntity.id;
lineEntity.lineColor = jsonEntity.lineColor;
lineEntity.lineWidth = jsonEntity.lineWidth;
lineEntity.lineDash = jsonEntity.lineDash;
return lineEntity;
}
@@ -1,6 +1,6 @@
import {Box, Line, Point, Segment, Vector} from '@flatten-js/core';
import {minBy, round} from 'es-toolkit';
import {max, min} from 'es-toolkit/compat';
import { Box, Line, Point, Segment, Vector } from '@flatten-js/core';
import { minBy, round } from 'es-toolkit';
import { max, min } from 'es-toolkit/compat';
import {
ARROW_HEAD_LENGTH,
ARROW_HEAD_WIDTH,
@@ -12,15 +12,32 @@ import {
MEASUREMENT_ORIGIN_MARGIN,
TO_RADIANS,
} from '../App.consts';
import type {Shape, SnapPoint} from '../App.types';
import type {DrawController} from '../drawControllers/DrawController';
import {pointDistance} from '../helpers/distance-between-points';
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';
import type {LineEntity} from './LineEntity.ts';
import type { Shape, SnapPoint } from '../App.types';
import type { DrawController } from '../drawControllers/DrawController';
import { pointDistance } from '../helpers/distance-between-points';
import { isPointEqual } from '../helpers/is-point-equal';
import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import { scalePoint } from '../helpers/scale-point';
import {
getActiveLayerId,
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 {
public id: string = crypto.randomUUID();
@@ -77,30 +94,41 @@ export class MeasurementEntity implements Entity {
.clone()
.translate(vectorPerpendicularFromLineTowardsOffsetPoint);
// Screen-pixel constants are converted to world units so annotation size stays zoom-independent
const worldFactor = annotationWorldFactor();
// Start of the perpendicular lines
const offsetStartPointMargin = this.startPoint
.clone()
.translate(
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_ORIGIN_MARGIN)
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(
MEASUREMENT_ORIGIN_MARGIN / worldFactor
)
);
const offsetEndPointMargin = this.endPoint
.clone()
.translate(
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_ORIGIN_MARGIN)
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(
MEASUREMENT_ORIGIN_MARGIN / worldFactor
)
);
// End of the perpendicular lines
const offsetStartPointExtend = offsetStartPoint
.clone()
.translate(
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_EXTENSION_LENGTH)
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(
MEASUREMENT_EXTENSION_LENGTH / worldFactor
)
);
const offsetEndPointExtend = offsetEndPoint
.clone()
.translate(
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(MEASUREMENT_EXTENSION_LENGTH)
vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(
MEASUREMENT_EXTENSION_LENGTH / worldFactor
)
);
// Location for label
@@ -108,8 +136,8 @@ export class MeasurementEntity implements Entity {
(offsetStartPoint.x + offsetEndPoint.x) / 2,
(offsetStartPoint.y + offsetEndPoint.y) / 2
);
const textHeight = MEASUREMENT_FONT_SIZE;
const totalOffset = MEASUREMENT_LABEL_OFFSET + textHeight / 2;
const textHeight = MEASUREMENT_FONT_SIZE / worldFactor;
const totalOffset = MEASUREMENT_LABEL_OFFSET / worldFactor + textHeight / 2;
const midpointMeasurementLineOffset = midpointMeasurementLine
.clone()
.translate(vectorPerpendicularFromLineTowardsOffsetPointUnit.multiply(totalOffset));
@@ -163,20 +191,21 @@ export class MeasurementEntity implements Entity {
isHighlighted: boolean,
isSelected: boolean
): 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 vectorFromEndToStartUnit = vectorFromEndToStart.normalize();
const baseOfArrow = endPoint
.clone()
.translate(vectorFromEndToStartUnit.multiply(ARROW_HEAD_LENGTH * screenScale));
.translate(vectorFromEndToStartUnit.multiply(ARROW_HEAD_LENGTH / worldFactor));
const perpendicularVector1 = vectorFromEndToStartUnit.rotate(90 * TO_RADIANS);
const perpendicularVector2 = vectorFromEndToStartUnit.rotate(-90 * TO_RADIANS);
const leftCornerOfArrow = baseOfArrow
.clone()
.translate(perpendicularVector1.multiply(ARROW_HEAD_WIDTH * screenScale));
.translate(perpendicularVector1.multiply(ARROW_HEAD_WIDTH / worldFactor));
const rightCornerOfArrow = baseOfArrow
.clone()
.translate(perpendicularVector2.multiply(ARROW_HEAD_WIDTH * screenScale));
.translate(perpendicularVector2.multiply(ARROW_HEAD_WIDTH / worldFactor));
drawController.setLineStyles(
isHighlighted,
@@ -268,7 +297,7 @@ export class MeasurementEntity implements Entity {
drawController.drawText(distance, midpointMeasurementLineOffset, {
textAlign: 'center',
textDirection: finalTextDirection,
fontSize: MEASUREMENT_FONT_SIZE,
fontSize: MEASUREMENT_FONT_SIZE / (drawController.getScreenScale() || 1),
textColor: this.lineColor,
});
}
@@ -374,9 +403,10 @@ export class MeasurementEntity implements Entity {
const distance = String(
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
const textWidth = distance.length * MEASUREMENT_FONT_SIZE * 0.6;
const textWidth = (distance.length * MEASUREMENT_FONT_SIZE * 0.6) / worldFactor;
const { midpointMeasurementLineOffset, normalUnit } = drawPoints;
@@ -532,6 +562,7 @@ export class MeasurementEntity implements Entity {
type: EntityName.Measurement,
lineColor: this.lineColor,
lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId,
shapeData: {
startPoint: { x: this.startPoint.x, y: this.startPoint.y },
@@ -565,6 +596,7 @@ export class MeasurementEntity implements Entity {
measurementEntity.id = jsonEntity.id;
measurementEntity.lineColor = jsonEntity.lineColor;
measurementEntity.lineWidth = jsonEntity.lineWidth;
measurementEntity.lineDash = jsonEntity.lineDash;
return measurementEntity;
}
}
@@ -1,13 +1,13 @@
import type * as Flatten from '@flatten-js/core';
import {Box, Point, type Segment} from '@flatten-js/core';
import {type Shape, type SnapPoint, SnapPointType} from '../App.types';
import type {DrawController} from '../drawControllers/DrawController';
import {getExportColor} from '../helpers/get-export-color';
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';
import type {LineEntity} from './LineEntity.ts';
import { Box, Point, type Segment } from '@flatten-js/core';
import { type Shape, type SnapPoint, SnapPointType } from '../App.types';
import type { DrawController } from '../drawControllers/DrawController';
import { getExportColor } from '../helpers/get-export-color';
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';
import type { LineEntity } from './LineEntity.ts';
export class PointEntity implements Entity {
public id: string = crypto.randomUUID();
@@ -124,6 +124,7 @@ export class PointEntity implements Entity {
type: EntityName.Point,
lineColor: this.lineColor,
lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId,
shapeData: {
point: {
@@ -143,6 +144,7 @@ export class PointEntity implements Entity {
lineEntity.id = jsonEntity.id;
lineEntity.lineColor = jsonEntity.lineColor;
lineEntity.lineWidth = jsonEntity.lineWidth;
lineEntity.lineDash = jsonEntity.lineDash;
return lineEntity;
}
}
@@ -1,14 +1,14 @@
import type * as Flatten from '@flatten-js/core';
import {Box, type Point, type Segment} from '@flatten-js/core';
import {mapLimit} from 'blend-promise-utils';
import {compact, maxBy} from 'es-toolkit';
import {minBy} from 'es-toolkit/compat';
import type {Shape, SnapPoint} from '../App.types';
import type {DrawController} from '../drawControllers/DrawController';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts';
import {ArcEntity, type ArcJsonData} from './ArcEntity.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity';
import {LineEntity, type LineJsonData} from './LineEntity.ts';
import { Box, type Point, type Segment } from '@flatten-js/core';
import { mapLimit } from 'blend-promise-utils';
import { compact, maxBy } from 'es-toolkit';
import { minBy } from 'es-toolkit/compat';
import type { Shape, SnapPoint } from '../App.types';
import type { DrawController } from '../drawControllers/DrawController';
import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts';
import { ArcEntity, type ArcJsonData } from './ArcEntity.ts';
import { type Entity, EntityName, type JsonEntity } from './Entity';
import { LineEntity, type LineJsonData } from './LineEntity.ts';
export class PolyLineEntity implements Entity {
public id: string = crypto.randomUUID();
@@ -133,6 +133,7 @@ export class PolyLineEntity implements Entity {
type: EntityName.PolyLine,
lineColor: this.lineColor,
lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId,
shapeData: null,
children: compact(await mapLimit(this.entities, 20, (entity) => entity.toJson())),
@@ -178,6 +179,7 @@ export class PolyLineEntity implements Entity {
polyLineEntity.id = jsonEntity.id;
polyLineEntity.lineColor = jsonEntity.lineColor;
polyLineEntity.lineWidth = jsonEntity.lineWidth;
polyLineEntity.lineDash = jsonEntity.lineDash;
return polyLineEntity;
}
}
@@ -1,15 +1,15 @@
import type * as Flatten 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 {DrawController} from '../drawControllers/DrawController';
import {twoPointBoxToPolygon} from '../helpers/box-to-polygon';
import {getExportColor} from '../helpers/get-export-color';
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts';
import {polygonToSegments} from '../helpers/polygon-to-segments';
import {scalePoint} from '../helpers/scale-point';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity';
import type {LineEntity} from './LineEntity.ts';
import { type Box, Point, Polygon, Relations, type Segment, Vector } from '@flatten-js/core';
import { type Shape, type SnapPoint, SnapPointType } from '../App.types';
import type { DrawController } from '../drawControllers/DrawController';
import { twoPointBoxToPolygon } from '../helpers/box-to-polygon';
import { getExportColor } from '../helpers/get-export-color';
import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import { polygonToSegments } from '../helpers/polygon-to-segments';
import { scalePoint } from '../helpers/scale-point';
import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts';
import { type Entity, EntityName, type JsonEntity } from './Entity';
import type { LineEntity } from './LineEntity.ts';
export class RectangleEntity implements Entity {
public id: string = crypto.randomUUID();
@@ -176,6 +176,7 @@ export class RectangleEntity implements Entity {
type: EntityName.Rectangle,
lineColor: this.lineColor,
lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId,
shapeData: {
points: this.polygon.vertices.map((vertex) => ({
@@ -202,6 +203,7 @@ export class RectangleEntity implements Entity {
rectangleEntity.id = jsonEntity.id;
rectangleEntity.lineColor = jsonEntity.lineColor;
rectangleEntity.lineWidth = jsonEntity.lineWidth;
rectangleEntity.lineDash = jsonEntity.lineDash;
return rectangleEntity;
}
}
@@ -1,12 +1,12 @@
import {Box, Point, type Segment, Vector} from '@flatten-js/core';
import {cloneDeep} from 'es-toolkit/compat';
import type {Shape, SnapPoint} from '../App.types';
import {DEFAULT_TEXT_OPTIONS, type DrawController} from '../drawControllers/DrawController';
import {mirrorPointOverAxis} from '../helpers/mirror-point-over-axis.ts';
import {scalePoint} from '../helpers/scale-point.ts';
import {getActiveLayerId, isEntityHighlighted, isEntitySelected} from '../state.ts';
import {type Entity, EntityName, type JsonEntity} from './Entity';
import type {LineEntity} from './LineEntity.ts';
import { Box, Point, type Segment, Vector } from '@flatten-js/core';
import { cloneDeep } from 'es-toolkit/compat';
import type { Shape, SnapPoint } from '../App.types';
import { DEFAULT_TEXT_OPTIONS, type DrawController } from '../drawControllers/DrawController';
import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis.ts';
import { scalePoint } from '../helpers/scale-point.ts';
import { getActiveLayerId, isEntityHighlighted, isEntitySelected } from '../state.ts';
import { type Entity, EntityName, type JsonEntity } from './Entity';
import type { LineEntity } from './LineEntity.ts';
export interface TextOptions {
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 {
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,
lineColor: this.lineColor,
lineWidth: this.lineWidth,
lineDash: this.lineDash,
layerId: this.layerId,
shapeData: {
label: this.label,
@@ -181,6 +190,7 @@ export class TextEntity implements Entity {
textEntity.id = jsonEntity.id;
textEntity.lineColor = jsonEntity.lineColor;
textEntity.lineWidth = jsonEntity.lineWidth;
textEntity.lineDash = jsonEntity.lineDash ?? [];
return textEntity;
}
}
@@ -26,6 +26,8 @@ export enum StateVariable {
lastDrawTimestamp = 'lastDrawTimestamp',
activeLineColor = 'activeLineColor',
activeLineWidth = 'activeLineWidth',
activeLineDash = 'activeLineDash',
activeTextStyle = 'activeTextStyle',
layers = 'layers',
}
+37 -2
View File
@@ -121,15 +121,29 @@ let hoveredSnapPoints: HoverPoint[] = [];
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
*/
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
*/
@@ -170,6 +184,8 @@ export const getHoveredSnapPoints = () => hoveredSnapPoints;
export const getLastDrawTimestamp = () => lastDrawTimestamp;
export const getActiveLineColor = () => activeLineColor;
export const getActiveLineWidth = () => activeLineWidth;
export const getActiveLineDash = () => activeLineDash;
export const getActiveTextStyle = () => activeTextStyle;
export const getScreenCanvasDrawController = (): ScreenCanvasDrawController => {
if (!screenCanvasDrawController) {
throw new Error('getScreenCanvasDrawController() returned null');
@@ -331,6 +347,23 @@ export const setActiveLineWidth = (newWidth: number, triggerReact = true) => {
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) => {
layers = newLayers;
@@ -379,6 +412,8 @@ const reactStateVariables: StateVariable[] = [
StateVariable.angleStep,
StateVariable.activeLineColor,
StateVariable.activeLineWidth,
StateVariable.activeLineDash,
StateVariable.activeTextStyle,
StateVariable.screenZoom,
StateVariable.layers,
];
@@ -1,161 +1,164 @@
import {CircleEntity} from '../entities/CircleEntity';
import type {Point} from '@flatten-js/core';
import { CircleEntity } from '../entities/CircleEntity';
import type { Point } from '@flatten-js/core';
import {
addEntities,
getActiveLayerId,
getActiveLineColor,
getActiveLineWidth,
setAngleGuideOriginPoint,
setGhostHelperEntities,
setSelectedEntityIds,
setShouldDrawHelpers,
addEntities,
getActiveLayerId,
getActiveLineColor,
getActiveLineDash,
getActiveLineWidth,
setAngleGuideOriginPoint,
setGhostHelperEntities,
setSelectedEntityIds,
setShouldDrawHelpers,
} from '../state';
import type {DrawEvent, PointInputEvent, StateEvent, ToolContext,} from './tool.types';
import {Tool} from '../tools';
import {assign, createMachine} from 'xstate';
import {pointDistance} from '../helpers/distance-between-points';
import {LineState} from './line-tool.ts';
import {getPointFromEvent} from '../helpers/get-point-from-event.ts';
import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types';
import { Tool } from '../tools';
import { assign, createMachine } from 'xstate';
import { pointDistance } from '../helpers/distance-between-points';
import { LineState } from './line-tool.ts';
import { getPointFromEvent } from '../helpers/get-point-from-event.ts';
export interface CircleContext extends ToolContext {
centerPoint: Point | null;
centerPoint: Point | null;
}
export enum CircleState {
WAITING_FOR_CENTER_POINT = 'WAITING_FOR_CENTER_POINT',
WAITING_FOR_POINT_ON_CIRCLE = 'WAITING_FOR_POINT_ON_CIRCLE',
INIT = 'INIT',
WAITING_FOR_CENTER_POINT = 'WAITING_FOR_CENTER_POINT',
WAITING_FOR_POINT_ON_CIRCLE = 'WAITING_FOR_POINT_ON_CIRCLE',
INIT = 'INIT',
}
export enum CircleAction {
INIT_CIRCLE_TOOL = 'INIT_CIRCLE_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT',
DRAW_TEMP_CIRCLE = 'DRAW_TEMP_CIRCLE',
DRAW_FINAL_CIRCLE = 'DRAW_FINAL_CIRCLE',
INIT_CIRCLE_TOOL = 'INIT_CIRCLE_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT',
DRAW_TEMP_CIRCLE = 'DRAW_TEMP_CIRCLE',
DRAW_FINAL_CIRCLE = 'DRAW_FINAL_CIRCLE',
}
export const circleToolStateMachine = createMachine(
{
types: {} as {
context: CircleContext;
events: StateEvent;
},
context: {
centerPoint: null,
type: Tool.CIRCLE,
},
initial: CircleState.INIT,
states: {
[CircleState.INIT]: {
description: 'Initializing the circle tool',
always: {
actions: CircleAction.INIT_CIRCLE_TOOL,
target: CircleState.WAITING_FOR_CENTER_POINT,
},
},
[CircleState.WAITING_FOR_CENTER_POINT]: {
description: 'Select the center point of the circle tool',
meta: {
instructions: 'Select the center point of the circle',
},
on: {
MOUSE_CLICK: {
actions: CircleAction.RECORD_START_POINT,
target: CircleState.WAITING_FOR_POINT_ON_CIRCLE,
},
ABSOLUTE_POINT_INPUT: {
actions: CircleAction.RECORD_START_POINT,
target: CircleState.WAITING_FOR_POINT_ON_CIRCLE,
},
},
},
[CircleState.WAITING_FOR_POINT_ON_CIRCLE]: {
description: 'Select a point on the circle',
meta: {
instructions: 'Select the point on the circle',
},
on: {
DRAW: {
actions: CircleAction.DRAW_TEMP_CIRCLE,
},
MOUSE_CLICK: {
actions: CircleAction.DRAW_FINAL_CIRCLE,
target: CircleState.INIT,
},
NUMBER_INPUT: {
actions: CircleAction.DRAW_FINAL_CIRCLE,
target: LineState.INIT,
},
ABSOLUTE_POINT_INPUT: {
actions: CircleAction.DRAW_FINAL_CIRCLE,
target: LineState.INIT,
},
RELATIVE_POINT_INPUT: {
actions: CircleAction.DRAW_FINAL_CIRCLE,
target: LineState.INIT,
},
ESC: {
target: CircleState.INIT,
},
},
},
},
},
{
actions: {
[CircleAction.INIT_CIRCLE_TOOL]: assign(() => {
setShouldDrawHelpers(true);
setGhostHelperEntities([]);
setSelectedEntityIds([]);
setAngleGuideOriginPoint(null);
return {
centerPoint: null,
};
}),
[CircleAction.RECORD_START_POINT]: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint);
return {
centerPoint: startPoint,
};
}),
[CircleAction.DRAW_TEMP_CIRCLE]: ({ context, event }) => {
const activeCircle = new CircleEntity(
getActiveLayerId(),
context.centerPoint as Point,
pointDistance(
(event as DrawEvent).drawController.getWorldMouseLocation(),
context.centerPoint as Point,
),
);
activeCircle.lineColor = getActiveLineColor();
activeCircle.lineWidth = getActiveLineWidth();
setGhostHelperEntities([activeCircle]);
},
[CircleAction.DRAW_FINAL_CIRCLE]: assign(({ context, event }) => {
if (!context.centerPoint) {
throw new Error(
'Trying to DRAW_FINAL_CIRCLE when centerPoint is not yet defined in circle tool',
);
}
const pointOnCircle: Point = getPointFromEvent(
context.centerPoint,
event as PointInputEvent,
);
const activeCircle = new CircleEntity(
getActiveLayerId(),
context.centerPoint as Point,
pointDistance(pointOnCircle, context.centerPoint as Point),
);
activeCircle.lineColor = getActiveLineColor();
activeCircle.lineWidth = getActiveLineWidth();
addEntities([activeCircle], true);
{
types: {} as {
context: CircleContext;
events: StateEvent;
},
context: {
centerPoint: null,
type: Tool.CIRCLE,
},
initial: CircleState.INIT,
states: {
[CircleState.INIT]: {
description: 'Initializing the circle tool',
always: {
actions: CircleAction.INIT_CIRCLE_TOOL,
target: CircleState.WAITING_FOR_CENTER_POINT,
},
},
[CircleState.WAITING_FOR_CENTER_POINT]: {
description: 'Select the center point of the circle tool',
meta: {
instructions: 'Select the center point of the circle',
},
on: {
MOUSE_CLICK: {
actions: CircleAction.RECORD_START_POINT,
target: CircleState.WAITING_FOR_POINT_ON_CIRCLE,
},
ABSOLUTE_POINT_INPUT: {
actions: CircleAction.RECORD_START_POINT,
target: CircleState.WAITING_FOR_POINT_ON_CIRCLE,
},
},
},
[CircleState.WAITING_FOR_POINT_ON_CIRCLE]: {
description: 'Select a point on the circle',
meta: {
instructions: 'Select the point on the circle',
},
on: {
DRAW: {
actions: CircleAction.DRAW_TEMP_CIRCLE,
},
MOUSE_CLICK: {
actions: CircleAction.DRAW_FINAL_CIRCLE,
target: CircleState.INIT,
},
NUMBER_INPUT: {
actions: CircleAction.DRAW_FINAL_CIRCLE,
target: LineState.INIT,
},
ABSOLUTE_POINT_INPUT: {
actions: CircleAction.DRAW_FINAL_CIRCLE,
target: LineState.INIT,
},
RELATIVE_POINT_INPUT: {
actions: CircleAction.DRAW_FINAL_CIRCLE,
target: LineState.INIT,
},
ESC: {
target: CircleState.INIT,
},
},
},
},
},
{
actions: {
[CircleAction.INIT_CIRCLE_TOOL]: assign(() => {
setShouldDrawHelpers(true);
setGhostHelperEntities([]);
setSelectedEntityIds([]);
setAngleGuideOriginPoint(null);
return {
centerPoint: null,
};
}),
[CircleAction.RECORD_START_POINT]: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint);
return {
centerPoint: startPoint,
};
}),
[CircleAction.DRAW_TEMP_CIRCLE]: ({ context, event }) => {
const activeCircle = new CircleEntity(
getActiveLayerId(),
context.centerPoint as Point,
pointDistance(
(event as DrawEvent).drawController.getWorldMouseLocation(),
context.centerPoint as Point
)
);
activeCircle.lineColor = getActiveLineColor();
activeCircle.lineWidth = getActiveLineWidth();
activeCircle.lineDash = getActiveLineDash();
setGhostHelperEntities([activeCircle]);
},
[CircleAction.DRAW_FINAL_CIRCLE]: assign(({ context, event }) => {
if (!context.centerPoint) {
throw new Error(
'Trying to DRAW_FINAL_CIRCLE when centerPoint is not yet defined in circle tool'
);
}
const pointOnCircle: Point = getPointFromEvent(
context.centerPoint,
event as PointInputEvent
);
const activeCircle = new CircleEntity(
getActiveLayerId(),
context.centerPoint as Point,
pointDistance(pointOnCircle, context.centerPoint as Point)
);
activeCircle.lineColor = getActiveLineColor();
activeCircle.lineWidth = getActiveLineWidth();
activeCircle.lineDash = getActiveLineDash();
addEntities([activeCircle], true);
setGhostHelperEntities([]);
return {
centerPoint: null,
};
}),
},
},
setGhostHelperEntities([]);
return {
centerPoint: null,
};
}),
},
}
);
@@ -1,171 +1,169 @@
import type {Point} from '@flatten-js/core';
import {LineEntity} from '../entities/LineEntity';
import type { Point } from '@flatten-js/core';
import { LineEntity } from '../entities/LineEntity';
import {
addEntities,
getActiveLayerId,
getActiveLineColor,
getActiveLineWidth,
setActiveToolActor,
setAngleGuideOriginPoint,
setGhostHelperEntities,
setSelectedEntityIds,
setShouldDrawHelpers,
addEntities,
getActiveLayerId,
getActiveLineColor,
getActiveLineDash,
getActiveLineWidth,
setActiveToolActor,
setAngleGuideOriginPoint,
setGhostHelperEntities,
setSelectedEntityIds,
setShouldDrawHelpers,
} from '../state';
import {Tool} from '../tools';
import {Actor, assign, createMachine} from 'xstate';
import type {DrawEvent, PointInputEvent, StateEvent, ToolContext,} from './tool.types';
import {selectToolStateMachine} from './select-tool.ts';
import {getPointFromEvent} from '../helpers/get-point-from-event.ts';
import { Tool } from '../tools';
import { Actor, assign, createMachine } from 'xstate';
import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types';
import { selectToolStateMachine } from './select-tool.ts';
import { getPointFromEvent } from '../helpers/get-point-from-event.ts';
export interface LineContext extends ToolContext {
startPoint: Point | null;
startPoint: Point | null;
}
export enum LineState {
INIT = 'INIT',
WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT',
WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT',
INIT = 'INIT',
WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT',
WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT',
}
export enum LineAction {
INIT_LINE_TOOL = 'INIT_LINE_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT',
DRAW_TEMP_LINE = 'DRAW_TEMP_LINE',
DRAW_FINAL_LINE = 'DRAW_FINAL_LINE',
SWITCH_TO_SELECT_TOOL = 'SWITCH_TO_SELECT_TOOL',
INIT_LINE_TOOL = 'INIT_LINE_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT',
DRAW_TEMP_LINE = 'DRAW_TEMP_LINE',
DRAW_FINAL_LINE = 'DRAW_FINAL_LINE',
SWITCH_TO_SELECT_TOOL = 'SWITCH_TO_SELECT_TOOL',
}
export const lineToolStateMachine = createMachine(
{
types: {} as {
context: LineContext;
events: StateEvent;
},
context: {
startPoint: null,
type: Tool.LINE,
},
initial: LineState.INIT,
states: {
[LineState.INIT]: {
description: 'Initializing the line tool',
always: {
actions: LineAction.INIT_LINE_TOOL,
target: LineState.WAITING_FOR_START_POINT,
},
},
[LineState.WAITING_FOR_START_POINT]: {
description: 'Select the start point of the line',
meta: {
instructions: 'Select the start point of the line',
},
on: {
MOUSE_CLICK: {
actions: LineAction.RECORD_START_POINT,
target: LineState.WAITING_FOR_END_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: LineAction.RECORD_START_POINT,
target: LineState.WAITING_FOR_END_POINT,
},
ESC: {
actions: LineAction.SWITCH_TO_SELECT_TOOL,
},
},
},
[LineState.WAITING_FOR_END_POINT]: {
description: 'Select the end point of the line',
meta: {
instructions: 'Select the end point of the line',
},
on: {
DRAW: {
actions: LineAction.DRAW_TEMP_LINE,
},
MOUSE_CLICK: {
actions: LineAction.DRAW_FINAL_LINE,
target: LineState.WAITING_FOR_END_POINT,
},
NUMBER_INPUT: {
actions: LineAction.DRAW_FINAL_LINE,
target: LineState.WAITING_FOR_END_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: LineAction.DRAW_FINAL_LINE,
target: LineState.WAITING_FOR_END_POINT,
},
RELATIVE_POINT_INPUT: {
actions: LineAction.DRAW_FINAL_LINE,
target: LineState.WAITING_FOR_END_POINT,
},
ESC: {
target: LineState.INIT,
},
ENTER: {
target: LineState.INIT,
},
},
},
},
},
{
actions: {
[LineAction.INIT_LINE_TOOL]: assign(() => {
setShouldDrawHelpers(true);
setSelectedEntityIds([]);
setGhostHelperEntities([]);
setAngleGuideOriginPoint(null);
return {
startPoint: null,
};
}),
[LineAction.RECORD_START_POINT]: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint);
return {
startPoint,
};
}),
[LineAction.DRAW_TEMP_LINE]: ({ context, event }) => {
const activeLine = new LineEntity(
getActiveLayerId(),
context.startPoint as Point,
(event as DrawEvent).drawController.getWorldMouseLocation(),
);
activeLine.lineColor = getActiveLineColor();
activeLine.lineWidth = getActiveLineWidth();
setGhostHelperEntities([activeLine]);
},
[LineAction.DRAW_FINAL_LINE]: assign(({ context, event }) => {
if (!context.startPoint) {
throw new Error(
'Start point is not set during DRAW_FINAL_LINE in LineEntity',
);
}
{
types: {} as {
context: LineContext;
events: StateEvent;
},
context: {
startPoint: null,
type: Tool.LINE,
},
initial: LineState.INIT,
states: {
[LineState.INIT]: {
description: 'Initializing the line tool',
always: {
actions: LineAction.INIT_LINE_TOOL,
target: LineState.WAITING_FOR_START_POINT,
},
},
[LineState.WAITING_FOR_START_POINT]: {
description: 'Select the start point of the line',
meta: {
instructions: 'Select the start point of the line',
},
on: {
MOUSE_CLICK: {
actions: LineAction.RECORD_START_POINT,
target: LineState.WAITING_FOR_END_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: LineAction.RECORD_START_POINT,
target: LineState.WAITING_FOR_END_POINT,
},
ESC: {
actions: LineAction.SWITCH_TO_SELECT_TOOL,
},
},
},
[LineState.WAITING_FOR_END_POINT]: {
description: 'Select the end point of the line',
meta: {
instructions: 'Select the end point of the line',
},
on: {
DRAW: {
actions: LineAction.DRAW_TEMP_LINE,
},
MOUSE_CLICK: {
actions: LineAction.DRAW_FINAL_LINE,
target: LineState.WAITING_FOR_END_POINT,
},
NUMBER_INPUT: {
actions: LineAction.DRAW_FINAL_LINE,
target: LineState.WAITING_FOR_END_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: LineAction.DRAW_FINAL_LINE,
target: LineState.WAITING_FOR_END_POINT,
},
RELATIVE_POINT_INPUT: {
actions: LineAction.DRAW_FINAL_LINE,
target: LineState.WAITING_FOR_END_POINT,
},
ESC: {
target: LineState.INIT,
},
ENTER: {
target: LineState.INIT,
},
},
},
},
},
{
actions: {
[LineAction.INIT_LINE_TOOL]: assign(() => {
setShouldDrawHelpers(true);
setSelectedEntityIds([]);
setGhostHelperEntities([]);
setAngleGuideOriginPoint(null);
return {
startPoint: null,
};
}),
[LineAction.RECORD_START_POINT]: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint);
return {
startPoint,
};
}),
[LineAction.DRAW_TEMP_LINE]: ({ context, event }) => {
const activeLine = new LineEntity(
getActiveLayerId(),
context.startPoint as Point,
(event as DrawEvent).drawController.getWorldMouseLocation()
);
activeLine.lineColor = getActiveLineColor();
activeLine.lineWidth = getActiveLineWidth();
activeLine.lineDash = getActiveLineDash();
setGhostHelperEntities([activeLine]);
},
[LineAction.DRAW_FINAL_LINE]: assign(({ context, event }) => {
if (!context.startPoint) {
throw new Error('Start point is not set during DRAW_FINAL_LINE in LineEntity');
}
const endPoint = getPointFromEvent(
context.startPoint,
event as PointInputEvent,
);
const activeLine = new LineEntity(
getActiveLayerId(),
context.startPoint as Point,
endPoint,
);
activeLine.lineColor = getActiveLineColor();
activeLine.lineWidth = getActiveLineWidth();
addEntities([activeLine], true);
const endPoint = getPointFromEvent(context.startPoint, event as PointInputEvent);
const activeLine = new LineEntity(
getActiveLayerId(),
context.startPoint as Point,
endPoint
);
activeLine.lineColor = getActiveLineColor();
activeLine.lineWidth = getActiveLineWidth();
activeLine.lineDash = getActiveLineDash();
addEntities([activeLine], true);
// Keep drawing from the last point
setGhostHelperEntities([new LineEntity(getActiveLayerId(), endPoint, endPoint)]);
setAngleGuideOriginPoint(endPoint);
return {
startPoint: endPoint,
};
}),
[LineAction.SWITCH_TO_SELECT_TOOL]: () => {
setActiveToolActor(new Actor(selectToolStateMachine));
},
},
},
// Keep drawing from the last point
setGhostHelperEntities([new LineEntity(getActiveLayerId(), endPoint, endPoint)]);
setAngleGuideOriginPoint(endPoint);
return {
startPoint: endPoint,
};
}),
[LineAction.SWITCH_TO_SELECT_TOOL]: () => {
setActiveToolActor(new Actor(selectToolStateMachine));
},
},
}
);
@@ -1,226 +1,221 @@
import {type Point, Vector} from '@flatten-js/core';
import {MeasurementEntity} from '../entities/MeasurementEntity';
import { type Point, Vector } from '@flatten-js/core';
import { MeasurementEntity } from '../entities/MeasurementEntity';
import {
addEntities,
getActiveLayerId,
getActiveLineColor,
getActiveLineWidth,
setAngleGuideOriginPoint,
setGhostHelperEntities,
setSelectedEntityIds,
setShouldDrawHelpers,
addEntities,
getActiveLayerId,
getActiveLineColor,
getActiveLineDash,
getActiveLineWidth,
setAngleGuideOriginPoint,
setGhostHelperEntities,
setSelectedEntityIds,
setShouldDrawHelpers,
} from '../state';
import {Tool} from '../tools';
import {assign, createMachine} from 'xstate';
import type {DrawEvent, PointInputEvent, StateEvent, ToolContext,} from './tool.types';
import {MEASUREMENT_DEFAULT_OFFSET, TO_RADIANS} from '../App.consts';
import {getPointFromEvent} from '../helpers/get-point-from-event.ts';
import {isPointEqual} from '../helpers/is-point-equal.ts';
import { Tool } from '../tools';
import { assign, createMachine } from 'xstate';
import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types';
import { MEASUREMENT_DEFAULT_OFFSET, TO_RADIANS } from '../App.consts';
import { getPointFromEvent } from '../helpers/get-point-from-event.ts';
import { isPointEqual } from '../helpers/is-point-equal.ts';
export interface MeasurementContext extends ToolContext {
startPoint: Point | null;
endPoint: Point | null;
startPoint: Point | null;
endPoint: Point | null;
}
export enum MeasurementState {
INIT = 'INIT',
WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT',
WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT',
WAITING_FOR_OFFSET = 'WAITING_FOR_OFFSET',
INIT = 'INIT',
WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT',
WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT',
WAITING_FOR_OFFSET = 'WAITING_FOR_OFFSET',
}
export enum MeasurementAction {
INIT_MEASUREMENT_TOOL = 'INIT_MEASUREMENT_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT',
RECORD_END_POINT = 'RECORD_END_POINT',
DRAW_TEMP_MEASUREMENT = 'DRAW_TEMP_MEASUREMENT',
DRAW_FINAL_MEASUREMENT = 'DRAW_FINAL_MEASUREMENT',
INIT_MEASUREMENT_TOOL = 'INIT_MEASUREMENT_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT',
RECORD_END_POINT = 'RECORD_END_POINT',
DRAW_TEMP_MEASUREMENT = 'DRAW_TEMP_MEASUREMENT',
DRAW_FINAL_MEASUREMENT = 'DRAW_FINAL_MEASUREMENT',
}
export const measurementToolStateMachine = createMachine(
{
types: {} as {
context: MeasurementContext;
events: StateEvent;
},
context: {
startPoint: null,
endPoint: null,
type: Tool.MEASUREMENT,
},
initial: MeasurementState.INIT,
states: {
[MeasurementState.INIT]: {
description: 'Initializing the line tool',
always: {
actions: MeasurementAction.INIT_MEASUREMENT_TOOL,
target: MeasurementState.WAITING_FOR_START_POINT,
},
},
[MeasurementState.WAITING_FOR_START_POINT]: {
description: 'Select the start point of the measurement',
meta: {
instructions: 'Select the start point of the measurement',
},
on: {
MOUSE_CLICK: {
actions: MeasurementAction.RECORD_START_POINT,
target: MeasurementState.WAITING_FOR_END_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: MeasurementAction.RECORD_START_POINT,
target: MeasurementState.WAITING_FOR_END_POINT,
},
},
},
[MeasurementState.WAITING_FOR_END_POINT]: {
description: 'Select the end point of the measurement',
meta: {
instructions: 'Select the end point of the measurement',
},
on: {
DRAW: {
actions: MeasurementAction.DRAW_TEMP_MEASUREMENT,
},
MOUSE_CLICK: {
actions: MeasurementAction.RECORD_END_POINT,
target: MeasurementState.WAITING_FOR_OFFSET,
},
NUMBER_INPUT: {
actions: MeasurementAction.RECORD_END_POINT,
target: MeasurementState.WAITING_FOR_OFFSET,
},
ABSOLUTE_POINT_INPUT: {
actions: MeasurementAction.RECORD_END_POINT,
target: MeasurementState.WAITING_FOR_OFFSET,
},
RELATIVE_POINT_INPUT: {
actions: MeasurementAction.RECORD_END_POINT,
target: MeasurementState.WAITING_FOR_OFFSET,
},
ESC: {
target: MeasurementState.INIT,
},
},
},
[MeasurementState.WAITING_FOR_OFFSET]: {
description: 'Select the offset to display the measurement at',
meta: {
instructions: 'Select the offset to display the measurement at',
},
on: {
DRAW: {
actions: MeasurementAction.DRAW_TEMP_MEASUREMENT,
},
MOUSE_CLICK: {
actions: MeasurementAction.DRAW_FINAL_MEASUREMENT,
target: MeasurementState.INIT,
},
NUMBER_INPUT: {
actions: MeasurementAction.DRAW_FINAL_MEASUREMENT,
target: MeasurementState.INIT,
},
ABSOLUTE_POINT_INPUT: {
actions: MeasurementAction.DRAW_FINAL_MEASUREMENT,
target: MeasurementState.INIT,
},
RELATIVE_POINT_INPUT: {
actions: MeasurementAction.DRAW_FINAL_MEASUREMENT,
target: MeasurementState.INIT,
},
ESC: {
target: MeasurementState.INIT,
},
},
},
},
},
{
actions: {
[MeasurementAction.INIT_MEASUREMENT_TOOL]: assign(() => {
setShouldDrawHelpers(true);
setSelectedEntityIds([]);
setGhostHelperEntities([]);
setAngleGuideOriginPoint(null);
return {
startPoint: null,
endPoint: null,
};
}),
[MeasurementAction.RECORD_START_POINT]: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint);
return {
startPoint,
};
}),
[MeasurementAction.RECORD_END_POINT]: assign(({ context, event }) => {
const endPoint = getPointFromEvent(
context.startPoint,
event as PointInputEvent,
);
setAngleGuideOriginPoint(endPoint);
return {
...context,
endPoint,
};
}),
[MeasurementAction.DRAW_TEMP_MEASUREMENT]: ({ context, event }) => {
const startPoint = context.startPoint as Point;
{
types: {} as {
context: MeasurementContext;
events: StateEvent;
},
context: {
startPoint: null,
endPoint: null,
type: Tool.MEASUREMENT,
},
initial: MeasurementState.INIT,
states: {
[MeasurementState.INIT]: {
description: 'Initializing the line tool',
always: {
actions: MeasurementAction.INIT_MEASUREMENT_TOOL,
target: MeasurementState.WAITING_FOR_START_POINT,
},
},
[MeasurementState.WAITING_FOR_START_POINT]: {
description: 'Select the start point of the measurement',
meta: {
instructions: 'Select the start point of the measurement',
},
on: {
MOUSE_CLICK: {
actions: MeasurementAction.RECORD_START_POINT,
target: MeasurementState.WAITING_FOR_END_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: MeasurementAction.RECORD_START_POINT,
target: MeasurementState.WAITING_FOR_END_POINT,
},
},
},
[MeasurementState.WAITING_FOR_END_POINT]: {
description: 'Select the end point of the measurement',
meta: {
instructions: 'Select the end point of the measurement',
},
on: {
DRAW: {
actions: MeasurementAction.DRAW_TEMP_MEASUREMENT,
},
MOUSE_CLICK: {
actions: MeasurementAction.RECORD_END_POINT,
target: MeasurementState.WAITING_FOR_OFFSET,
},
NUMBER_INPUT: {
actions: MeasurementAction.RECORD_END_POINT,
target: MeasurementState.WAITING_FOR_OFFSET,
},
ABSOLUTE_POINT_INPUT: {
actions: MeasurementAction.RECORD_END_POINT,
target: MeasurementState.WAITING_FOR_OFFSET,
},
RELATIVE_POINT_INPUT: {
actions: MeasurementAction.RECORD_END_POINT,
target: MeasurementState.WAITING_FOR_OFFSET,
},
ESC: {
target: MeasurementState.INIT,
},
},
},
[MeasurementState.WAITING_FOR_OFFSET]: {
description: 'Select the offset to display the measurement at',
meta: {
instructions: 'Select the offset to display the measurement at',
},
on: {
DRAW: {
actions: MeasurementAction.DRAW_TEMP_MEASUREMENT,
},
MOUSE_CLICK: {
actions: MeasurementAction.DRAW_FINAL_MEASUREMENT,
target: MeasurementState.INIT,
},
NUMBER_INPUT: {
actions: MeasurementAction.DRAW_FINAL_MEASUREMENT,
target: MeasurementState.INIT,
},
ABSOLUTE_POINT_INPUT: {
actions: MeasurementAction.DRAW_FINAL_MEASUREMENT,
target: MeasurementState.INIT,
},
RELATIVE_POINT_INPUT: {
actions: MeasurementAction.DRAW_FINAL_MEASUREMENT,
target: MeasurementState.INIT,
},
ESC: {
target: MeasurementState.INIT,
},
},
},
},
},
{
actions: {
[MeasurementAction.INIT_MEASUREMENT_TOOL]: assign(() => {
setShouldDrawHelpers(true);
setSelectedEntityIds([]);
setGhostHelperEntities([]);
setAngleGuideOriginPoint(null);
return {
startPoint: null,
endPoint: null,
};
}),
[MeasurementAction.RECORD_START_POINT]: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint);
return {
startPoint,
};
}),
[MeasurementAction.RECORD_END_POINT]: assign(({ context, event }) => {
const endPoint = getPointFromEvent(context.startPoint, event as PointInputEvent);
setAngleGuideOriginPoint(endPoint);
return {
...context,
endPoint,
};
}),
[MeasurementAction.DRAW_TEMP_MEASUREMENT]: ({ context, event }) => {
const startPoint = context.startPoint as Point;
let endPoint: Point;
let offsetPoint: Point;
if (!context.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 = (
event as DrawEvent
).drawController.getWorldMouseLocation();
let endPoint: Point;
let offsetPoint: Point;
if (!context.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 = (event as DrawEvent).drawController.getWorldMouseLocation();
if (isPointEqual(startPoint, endPoint)) {
return; // Cannot draw temp measurement when start and endpoint are equal
}
if (isPointEqual(startPoint, endPoint)) {
return; // Cannot draw temp measurement when start and endpoint are equal
}
const normalVector = new Vector(startPoint, endPoint)
.rotate(-90 * TO_RADIANS)
.normalize();
offsetPoint = startPoint
.clone()
.translate(normalVector.multiply(MEASUREMENT_DEFAULT_OFFSET));
} else {
// User has already selected a startPoint and endPoint
// The offsetPoint should be set to the mouse location
endPoint = context.endPoint as Point;
offsetPoint = (
event as DrawEvent
).drawController.getWorldMouseLocation();
}
const normalVector = new Vector(startPoint, endPoint)
.rotate(-90 * TO_RADIANS)
.normalize();
// Pixel constant → world units so the default offset is zoom-independent
const worldFactor = (event as DrawEvent).drawController.getScreenScale() || 1;
offsetPoint = startPoint
.clone()
.translate(normalVector.multiply(MEASUREMENT_DEFAULT_OFFSET / worldFactor));
} else {
// User has already selected a startPoint and endPoint
// The offsetPoint should be set to the mouse location
endPoint = context.endPoint as Point;
offsetPoint = (event as DrawEvent).drawController.getWorldMouseLocation();
}
const activeMeasurement = new MeasurementEntity(
getActiveLayerId(),
context.startPoint as Point,
endPoint,
offsetPoint,
);
activeMeasurement.lineColor = getActiveLineColor();
activeMeasurement.lineWidth = getActiveLineWidth();
setGhostHelperEntities([activeMeasurement]);
},
[MeasurementAction.DRAW_FINAL_MEASUREMENT]: ({ context, event }) => {
const offsetPoint = getPointFromEvent(
context.endPoint,
event as PointInputEvent,
);
const activeMeasurement = new MeasurementEntity(
getActiveLayerId(),
context.startPoint as Point,
context.endPoint as Point,
offsetPoint,
);
activeMeasurement.lineColor = getActiveLineColor();
activeMeasurement.lineWidth = getActiveLineWidth();
addEntities([activeMeasurement], true);
},
},
},
const activeMeasurement = new MeasurementEntity(
getActiveLayerId(),
context.startPoint as Point,
endPoint,
offsetPoint
);
activeMeasurement.lineColor = getActiveLineColor();
activeMeasurement.lineWidth = getActiveLineWidth();
activeMeasurement.lineDash = getActiveLineDash();
setGhostHelperEntities([activeMeasurement]);
},
[MeasurementAction.DRAW_FINAL_MEASUREMENT]: ({ context, event }) => {
const offsetPoint = getPointFromEvent(context.endPoint, event as PointInputEvent);
const activeMeasurement = new MeasurementEntity(
getActiveLayerId(),
context.startPoint as Point,
context.endPoint as Point,
offsetPoint
);
activeMeasurement.lineColor = getActiveLineColor();
activeMeasurement.lineWidth = getActiveLineWidth();
activeMeasurement.lineDash = getActiveLineDash();
addEntities([activeMeasurement], true);
},
},
}
);
@@ -1,154 +1,152 @@
import type {Point} from '@flatten-js/core';
import {RectangleEntity} from '../entities/RectangleEntity';
import type { Point } from '@flatten-js/core';
import { RectangleEntity } from '../entities/RectangleEntity';
import {
addEntities,
getActiveLayerId,
getActiveLineColor,
getActiveLineWidth,
setAngleGuideOriginPoint,
setGhostHelperEntities,
setSelectedEntityIds,
setShouldDrawHelpers,
addEntities,
getActiveLayerId,
getActiveLineColor,
getActiveLineDash,
getActiveLineWidth,
setAngleGuideOriginPoint,
setGhostHelperEntities,
setSelectedEntityIds,
setShouldDrawHelpers,
} from '../state';
import type {DrawEvent, PointInputEvent, StateEvent, ToolContext,} from './tool.types';
import {Tool} from '../tools';
import {assign, createMachine} from 'xstate';
import {getPointFromEvent} from '../helpers/get-point-from-event.ts';
import type { DrawEvent, PointInputEvent, StateEvent, ToolContext } from './tool.types';
import { Tool } from '../tools';
import { assign, createMachine } from 'xstate';
import { getPointFromEvent } from '../helpers/get-point-from-event.ts';
export interface RectangleContext extends ToolContext {
startPoint: Point | null;
startPoint: Point | null;
}
export enum RectangleState {
INIT = 'INIT',
WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT',
WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT',
INIT = 'INIT',
WAITING_FOR_START_POINT = 'WAITING_FOR_START_POINT',
WAITING_FOR_END_POINT = 'WAITING_FOR_END_POINT',
}
export enum RectangleAction {
INIT_RECTANGLE_TOOL = 'INIT_RECTANGLE_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT',
DRAW_TEMP_RECTANGLE = 'DRAW_TEMP_RECTANGLE',
DRAW_FINAL_RECTANGLE = 'DRAW_FINAL_RECTANGLE',
INIT_RECTANGLE_TOOL = 'INIT_RECTANGLE_TOOL',
RECORD_START_POINT = 'RECORD_START_POINT',
DRAW_TEMP_RECTANGLE = 'DRAW_TEMP_RECTANGLE',
DRAW_FINAL_RECTANGLE = 'DRAW_FINAL_RECTANGLE',
}
export const rectangleToolStateMachine = createMachine(
{
types: {} as {
context: RectangleContext;
events: StateEvent;
},
context: {
startPoint: null,
type: Tool.RECTANGLE,
},
initial: RectangleState.INIT,
states: {
[RectangleState.INIT]: {
description: 'Initializing the rectangle tool',
always: {
actions: RectangleAction.INIT_RECTANGLE_TOOL,
target: RectangleState.WAITING_FOR_START_POINT,
},
},
[RectangleState.WAITING_FOR_START_POINT]: {
description: 'Select the start point of the rectangle',
meta: {
instructions: 'Select the start point of the rectangle',
},
on: {
MOUSE_CLICK: {
actions: RectangleAction.RECORD_START_POINT,
target: RectangleState.WAITING_FOR_END_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: RectangleAction.RECORD_START_POINT,
target: RectangleState.WAITING_FOR_END_POINT,
},
},
},
[RectangleState.WAITING_FOR_END_POINT]: {
description: 'Select the end point of the rectangle',
meta: {
instructions: 'Select the end point of the rectangle',
},
on: {
DRAW: {
actions: RectangleAction.DRAW_TEMP_RECTANGLE,
},
MOUSE_CLICK: {
actions: RectangleAction.DRAW_FINAL_RECTANGLE,
target: RectangleState.INIT,
},
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
target: RectangleState.INIT,
},
ABSOLUTE_POINT_INPUT: {
actions: RectangleAction.DRAW_FINAL_RECTANGLE,
target: RectangleState.INIT,
},
RELATIVE_POINT_INPUT: {
actions: RectangleAction.DRAW_FINAL_RECTANGLE,
target: RectangleState.INIT,
},
ESC: {
target: RectangleState.INIT,
},
},
},
},
},
{
actions: {
[RectangleAction.INIT_RECTANGLE_TOOL]: () => {
setShouldDrawHelpers(true);
setGhostHelperEntities([]);
setSelectedEntityIds([]);
setAngleGuideOriginPoint(null);
},
[RectangleAction.RECORD_START_POINT]: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint);
return {
startPoint,
};
}),
[RectangleAction.DRAW_TEMP_RECTANGLE]: ({ context, event }) => {
if (!context.startPoint) {
throw new Error(
'[RECTANGLE]: calling draw without start point being set',
);
}
const activeRectangle = new RectangleEntity(
getActiveLayerId(),
context.startPoint as Point,
(event as DrawEvent).drawController.getWorldMouseLocation(),
);
activeRectangle.lineColor = getActiveLineColor();
activeRectangle.lineWidth = getActiveLineWidth();
setGhostHelperEntities([activeRectangle]);
},
[RectangleAction.DRAW_FINAL_RECTANGLE]: ({ context, event }) => {
if (!context.startPoint) {
throw Error(
'Trying to DRAW_FINAL_RECTANGLE when startPoint is not defined in rectangle-tool',
);
}
const endPoint = getPointFromEvent(
context.startPoint,
event as PointInputEvent,
);
{
types: {} as {
context: RectangleContext;
events: StateEvent;
},
context: {
startPoint: null,
type: Tool.RECTANGLE,
},
initial: RectangleState.INIT,
states: {
[RectangleState.INIT]: {
description: 'Initializing the rectangle tool',
always: {
actions: RectangleAction.INIT_RECTANGLE_TOOL,
target: RectangleState.WAITING_FOR_START_POINT,
},
},
[RectangleState.WAITING_FOR_START_POINT]: {
description: 'Select the start point of the rectangle',
meta: {
instructions: 'Select the start point of the rectangle',
},
on: {
MOUSE_CLICK: {
actions: RectangleAction.RECORD_START_POINT,
target: RectangleState.WAITING_FOR_END_POINT,
},
ABSOLUTE_POINT_INPUT: {
actions: RectangleAction.RECORD_START_POINT,
target: RectangleState.WAITING_FOR_END_POINT,
},
},
},
[RectangleState.WAITING_FOR_END_POINT]: {
description: 'Select the end point of the rectangle',
meta: {
instructions: 'Select the end point of the rectangle',
},
on: {
DRAW: {
actions: RectangleAction.DRAW_TEMP_RECTANGLE,
},
MOUSE_CLICK: {
actions: RectangleAction.DRAW_FINAL_RECTANGLE,
target: RectangleState.INIT,
},
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
target: RectangleState.INIT,
},
ABSOLUTE_POINT_INPUT: {
actions: RectangleAction.DRAW_FINAL_RECTANGLE,
target: RectangleState.INIT,
},
RELATIVE_POINT_INPUT: {
actions: RectangleAction.DRAW_FINAL_RECTANGLE,
target: RectangleState.INIT,
},
ESC: {
target: RectangleState.INIT,
},
},
},
},
},
{
actions: {
[RectangleAction.INIT_RECTANGLE_TOOL]: () => {
setShouldDrawHelpers(true);
setGhostHelperEntities([]);
setSelectedEntityIds([]);
setAngleGuideOriginPoint(null);
},
[RectangleAction.RECORD_START_POINT]: assign(({ event }) => {
const startPoint = getPointFromEvent(null, event as PointInputEvent);
setAngleGuideOriginPoint(startPoint);
return {
startPoint,
};
}),
[RectangleAction.DRAW_TEMP_RECTANGLE]: ({ context, event }) => {
if (!context.startPoint) {
throw new Error('[RECTANGLE]: calling draw without start point being set');
}
const activeRectangle = new RectangleEntity(
getActiveLayerId(),
context.startPoint as Point,
(event as DrawEvent).drawController.getWorldMouseLocation()
);
activeRectangle.lineColor = getActiveLineColor();
activeRectangle.lineWidth = getActiveLineWidth();
activeRectangle.lineDash = getActiveLineDash();
setGhostHelperEntities([activeRectangle]);
},
[RectangleAction.DRAW_FINAL_RECTANGLE]: ({ context, event }) => {
if (!context.startPoint) {
throw Error(
'Trying to DRAW_FINAL_RECTANGLE when startPoint is not defined in rectangle-tool'
);
}
const endPoint = getPointFromEvent(context.startPoint, event as PointInputEvent);
const activeRectangle = new RectangleEntity(
getActiveLayerId(),
context.startPoint as Point,
endPoint,
);
activeRectangle.lineColor = getActiveLineColor();
activeRectangle.lineWidth = getActiveLineWidth();
addEntities([activeRectangle], true);
},
},
},
const activeRectangle = new RectangleEntity(
getActiveLayerId(),
context.startPoint as Point,
endPoint
);
activeRectangle.lineColor = getActiveLineColor();
activeRectangle.lineWidth = getActiveLineWidth();
activeRectangle.lineDash = getActiveLineDash();
addEntities([activeRectangle], true);
},
},
}
);