feat(B07,B08): 워크플로 순서 교환 — 상세설계를 수량산출 앞으로
횡단설계(B06) 다음을 상세설계 → 수량산출 → 설계도서 순으로 재배열하고, 폴더 번호가 흐름과 일치하도록 이름을 맞바꾼다. - B08_DesignDetail → B07_DesignDetail, B07_Quantity → B08_Quantity (파일 접두어·식별자·라우트·locale 키 전량 스왑) - STAGE_KEYS 4=DESIGN_DETAIL, 5=QUANTITY 스왑 + 라우터 stage 리터럴 교체 - CAD 마운트 /b08-cad → /b07-cad (main.py·vite proxy·iframe URL), openwebcad Toolbar 라벨 B07로 수정 후 재빌드 - 유지: openwebcad postMessage 프로토콜 aislo:b08:*·패키지명(내부 식별자) - 기존 프로젝트 storage 폴더 rename + project_manifest 갱신, DB project_workflow_stages stage_no 4↔5 행 스왑 완료 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
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;
|
||||
textAlign: 'left' | 'center' | 'right';
|
||||
textColor: string;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
}
|
||||
|
||||
export class TextEntity implements Entity {
|
||||
public id: string = crypto.randomUUID();
|
||||
public lineColor = '#fff';
|
||||
public lineWidth = 1;
|
||||
public lineDash: number[] = [];
|
||||
public layerId: string;
|
||||
private readonly options: TextOptions;
|
||||
|
||||
constructor(
|
||||
layerId: string,
|
||||
private label: string,
|
||||
private basePoint: Point,
|
||||
options?: Partial<TextOptions>
|
||||
) {
|
||||
this.layerId = layerId;
|
||||
this.options = {
|
||||
...DEFAULT_TEXT_OPTIONS,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
public draw(
|
||||
drawController: DrawController,
|
||||
parentHighlighted?: boolean,
|
||||
parentSelected?: boolean
|
||||
): void {
|
||||
drawController.setLineStyles(
|
||||
parentHighlighted ?? isEntityHighlighted(this),
|
||||
parentSelected ?? isEntitySelected(this),
|
||||
this.lineColor,
|
||||
this.lineWidth,
|
||||
this.lineDash
|
||||
);
|
||||
drawController.drawText(this.label, this.basePoint, this.options);
|
||||
}
|
||||
|
||||
public move(x: number, y: number) {
|
||||
this.basePoint = this.basePoint.translate(x, y);
|
||||
}
|
||||
|
||||
public scale(scaleOrigin: Point, scaleFactor: number) {
|
||||
this.basePoint = scalePoint(this.basePoint, scaleOrigin, scaleFactor);
|
||||
this.options.fontSize = this.options.fontSize * scaleFactor; // TODO discuss if text should scale or not?
|
||||
}
|
||||
|
||||
public rotate(rotateOrigin: Point, angle: number) {
|
||||
this.basePoint = this.basePoint.rotate(angle, rotateOrigin);
|
||||
this.options.textDirection = this.options.textDirection.rotate(angle);
|
||||
}
|
||||
|
||||
public mirror(mirrorAxis: LineEntity) {
|
||||
this.basePoint = mirrorPointOverAxis(this.basePoint, mirrorAxis);
|
||||
this.options.textDirection = new Vector(
|
||||
new Point(0, 0),
|
||||
new Point(this.options.textDirection.x, this.options.textDirection.y)
|
||||
);
|
||||
}
|
||||
|
||||
public clone(): TextEntity {
|
||||
return new TextEntity(
|
||||
getActiveLayerId(),
|
||||
this.label,
|
||||
this.basePoint.clone(),
|
||||
cloneDeep(this.options)
|
||||
);
|
||||
}
|
||||
|
||||
public intersectsWithBox(box: Box): boolean {
|
||||
return box.contains(this.basePoint);
|
||||
}
|
||||
|
||||
public isContainedInBox(box: Box): boolean {
|
||||
return box.contains(this.basePoint);
|
||||
}
|
||||
|
||||
public getBoundingBox(): Box {
|
||||
// TODO find better way of determining the text bounding box
|
||||
return new Box(
|
||||
this.basePoint.x,
|
||||
this.basePoint.y,
|
||||
this.basePoint.x + this.options.fontSize * this.label.length,
|
||||
this.basePoint.y + this.options.fontSize
|
||||
);
|
||||
}
|
||||
|
||||
public getTextOptions(): TextOptions {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
public setTextOptions(newOptions: Partial<Omit<TextOptions, 'textDirection'>>): void {
|
||||
Object.assign(this.options, newOptions);
|
||||
}
|
||||
|
||||
public getLabel(): string {
|
||||
return this.label;
|
||||
}
|
||||
|
||||
/** 테이블 값 셀 등 기존 텍스트 내용을 직접 수정한다 (aislo 더블클릭 편집용). */
|
||||
public setLabel(newLabel: string): void {
|
||||
this.label = newLabel;
|
||||
}
|
||||
|
||||
public getShape(): Shape | null {
|
||||
return null; // TODO see why we need to get the shape out of an entity
|
||||
}
|
||||
|
||||
public getSnapPoints(): SnapPoint[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public getIntersections(_entity: Entity): Point[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
public getFirstPoint(): Point | null {
|
||||
return this.basePoint;
|
||||
}
|
||||
|
||||
public distanceTo(shape: Shape): [number, Segment] | null {
|
||||
return this.basePoint.distanceTo(shape);
|
||||
}
|
||||
|
||||
public getSvgString(): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
public getType(): EntityName {
|
||||
return EntityName.Text;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public containsPointOnShape(_point: Point): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async toJson(): Promise<JsonEntity<TextJsonData> | null> {
|
||||
return {
|
||||
id: this.id,
|
||||
type: EntityName.Text,
|
||||
lineColor: this.lineColor,
|
||||
lineWidth: this.lineWidth,
|
||||
lineDash: this.lineDash,
|
||||
layerId: this.layerId,
|
||||
shapeData: {
|
||||
label: this.label,
|
||||
basePoint: { x: this.basePoint.x, y: this.basePoint.y },
|
||||
options: {
|
||||
textDirection: {
|
||||
x: this.options.textDirection.x,
|
||||
y: this.options.textDirection.y,
|
||||
},
|
||||
textAlign: this.options.textAlign,
|
||||
textColor: this.options.textColor,
|
||||
fontSize: this.options.fontSize,
|
||||
fontFamily: this.options.fontFamily,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static async fromJson(jsonEntity: JsonEntity<TextJsonData>): Promise<TextEntity> {
|
||||
if (!jsonEntity.shapeData) {
|
||||
throw new Error('Invalid JSON entity of type Text: missing shapeData');
|
||||
}
|
||||
const textEntity = new TextEntity(
|
||||
jsonEntity.layerId || getActiveLayerId(),
|
||||
jsonEntity.shapeData.label,
|
||||
new Point(jsonEntity.shapeData.basePoint.x, jsonEntity.shapeData.basePoint.y),
|
||||
{
|
||||
textDirection: new Vector(
|
||||
jsonEntity.shapeData.options.textDirection.x,
|
||||
jsonEntity.shapeData.options.textDirection.y
|
||||
),
|
||||
textAlign: jsonEntity.shapeData.options.textAlign,
|
||||
textColor: jsonEntity.shapeData.options.textColor,
|
||||
fontSize: jsonEntity.shapeData.options.fontSize,
|
||||
fontFamily: jsonEntity.shapeData.options.fontFamily,
|
||||
}
|
||||
);
|
||||
textEntity.id = jsonEntity.id;
|
||||
textEntity.lineColor = jsonEntity.lineColor;
|
||||
textEntity.lineWidth = jsonEntity.lineWidth;
|
||||
textEntity.lineDash = jsonEntity.lineDash ?? [];
|
||||
return textEntity;
|
||||
}
|
||||
}
|
||||
|
||||
export interface TextJsonData {
|
||||
label: string;
|
||||
basePoint: { x: number; y: number };
|
||||
options: {
|
||||
textDirection: { x: number; y: number };
|
||||
textAlign: 'left' | 'center' | 'right';
|
||||
textColor: string;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user