260719_9
This commit is contained in:
@@ -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',
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user