From 87545e5df17ca5188d15768dec47079917584daf Mon Sep 17 00:00:00 2001 From: Jaeyoung Lee Date: Fri, 12 Jun 2026 13:58:15 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=9A=A9=EB=A7=88=EB=A3=A8(skeleton)?= =?UTF-8?q?=20=EC=A7=80=EB=B6=95=20=EC=99=B8=EA=B3=BD=20=EC=B2=98=EB=A7=88?= =?UTF-8?q?=EC=84=A0=20=ED=8E=B8=EC=A7=91=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [작업내용] : - 추녀 연장(drawBaselineToRooflineHelpers) 후 splitRoofLinesToInnerLines 호출 → 처마변을 편집 가능한 innerLine(hip/roofLine)으로 등록 - 처마선 innerLine 이 외곽선을 전담하므로 roof 원본 외곽(stroke) 숨김 처리 - 설계 문서 정정: 대상 브랜치 roofline-edit, §4 리스크1 검증 완료, §9 측정 시점 정정 (추녀 연장 후 종점이 처마변 위에 정합 → split 은 연장 다음에 호출) Co-Authored-By: Claude Opus 4.8 (1M context) --- ...026-06-01-skeleton-roofline-edit-design.md | 46 +++++++++++++++++-- src/util/skeleton-utils.js | 9 +++- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/docs/plans/2026-06-01-skeleton-roofline-edit-design.md b/docs/plans/2026-06-01-skeleton-roofline-edit-design.md index 82414cae..9e97a2d2 100644 --- a/docs/plans/2026-06-01-skeleton-roofline-edit-design.md +++ b/docs/plans/2026-06-01-skeleton-roofline-edit-design.md @@ -1,7 +1,7 @@ # 용마루(skeleton) 경로 외곽 지붕선 편집 기능 — 설계 - 작성일: 2026-06-01 -- 대상 브랜치: `feature/redo-undo` +- 대상 브랜치: `feature/roofline-edit` - 관련 파일: `src/util/skeleton-utils.js`, `src/util/qpolygon-utils.js`, `src/hooks/useContextMenu.js` ## 1. 배경 / 문제 @@ -66,13 +66,13 @@ splitRoofLinesToInnerLines(roof, canvas, textMode) → QLine[] - **`drawRoofByAttribute` 교체**: 인라인 블록을 함수 호출로 교체. 순수 추출이므로 변별 경로 동작 불변. ### 3-2. skeleton 경로 변경 -- `createInnerLinesFromSkeleton` 의 `isOuterEdge` 숨김 처리(`:1636-1643` 의 `visible:false`, `:1624` 의 `selectable:false`) 제거. -- 내부선(용마루/추녀) 생성 완료 후 `splitRoofLinesToInnerLines(roof, canvas, textMode)` 호출 → 외곽 지붕선이 변별 경로와 동일한 형태로 `roof.innerLines` 에 합류. +- `createInnerLinesFromSkeleton` 의 `isOuterEdge`(SK eaves) 숨김 처리는 **유지**. (§9 정정: SK eaves 라인은 baseLine 기준이라, 노출하면 처마변 split 결과와 offset 만큼 어긋나 이중 표시됨. 처마 외곽선은 아래 split 으로 별도 innerLine 화.) +- **추녀 연장(`drawBaselineToRooflineHelpers`, `:1285`) 완료 후** `splitRoofLinesToInnerLines(roof, canvas, textMode)` 호출 → 추녀 종점이 처마변 위에 있는 상태라 split 정합 성립, 외곽 지붕선이 변별 경로와 동일한 형태로 `roof.innerLines` 에 합류. (⚠️ 연장 **전**에 호출하면 종점이 baseLine 위라 분할점을 못 잡음 — §9 참조) - `drawHelpLine` 재호출 시마다 자동 재실행되어 편집 후 재그리기에도 외곽 지붕선 유지. ## 4. 리스크 & 구현 전 검증 -1. **SK 추녀 끝점 ↔ `roof.lines` 정합성**: split 은 "innerLine 끝점이 `roof.lines` 위에 있다"(`isPointOnLineNew`, tol 0.1)를 전제로 한다. SK 가 만든 hip 끝점이 처마 외곽선 위에 실제로 떨어지는지 **실측 확인 필요**. 안 맞으면 split 이 헛돌아 외곽선이 통째로 roofLine 1개로만 그려진다. → 구현 첫 단계에서 검증. +1. **SK 추녀 끝점 ↔ `roof.lines` 정합성** — ✅ **검증 완료 (2026-06-12, §9 참조)**: **연장 후 정합 OK**. SK 골조는 `wall.baseLines` 기준이라 추녀 끝점이 처음엔 baseLine 코너에 있으나(연장 전 `onRoof=false`), `drawBaselineToRooflineHelpers`(`:1285`)가 추녀를 처마변까지 **연장**해 종점을 처마변 위로 옮긴다(연장 후 4코너 전부 `diffRoof=0`, `onRoof=true`). → **split 을 연장 다음에 호출하면 정합 자동 성립**. (초기 측정이 연장 전에 찍혀 "구조적 불일치"로 오판했음 — §9-0 참조) 2. **순환 import**: `qpolygon-utils ↔ skeleton-utils` 상호 import 여부 점검. 순환이면 split 함수를 제3의 모듈(예: canvas-util)로 추출하거나 의존성 정리. 3. **기존 `isOuterEdge` roofLine 제거 영향**: 숨긴 라인을 참조하는 곳(치수 계산, 플랜 저장/복원 등)이 있는지 확인 후 제거. @@ -159,3 +159,41 @@ opts.degreeOf?: (parentLine) => number // 부모 외곽변 → 경사각(도). ### 8-5. 리스크 - **분할점 부재**: 한쪽흐름은 내부 보조선이 없어 외곽변이 1:1 통짜 라인으로 그려진다 → 부모 변 type 매칭이 단순(분할 시에도 조각은 부모 type 상속). split 의 끝점 정합성(`isPointOnLineNew`, tol 0.1) 자체는 분할점이 없으면 무의미하므로 `else` 통짜 경로(`:5493`)로 안전하게 떨어진다. - **`isGable` 판정원**: `drawShedRoof` 의 `gables` 분류(`:1653`)와 동일 기준(`attributes.type === GABLE`)을 써야 보정 대상이 일치. + +--- + +## 9. SK 끝점 ↔ 처마변 정합 — 실측 결과 (2026-06-12, 측정 시점 정정) + +§4 리스크1 을 임시 계측(`skeleton-utils.js` 의 `logSkRoofLineAlignment`)으로 추녀 연장 **전/후** 2단계 실측한 결과. + +### 9-0. ⚠️ 측정 시점 정정 (중요) +1차 측정은 `createInnerLinesFromSkeleton` **직후**(`:1279`, 추녀 연장 전)에 찍혀, 추녀 종점이 baseLine 코너에 있는 상태(`onRoof=false`, `perpRoof=offset`)만 보고 **"구조적 불일치 → 투영 보정 필요"로 오판**했다. 실제로는 그 직후 `drawBaselineToRooflineHelpers`(`:1285`)가 추녀를 처마변까지 연장한다. 연장 **후**(`afterExt`) 측정에서 정합이 성립함을 확인 → 아래는 정정된 결론. **(투영 보정·SK 입력 교체·연장 보정 설계는 모두 폐기. 불필요했음.)** + +### 9-1. 실측 결과 (용마루 우진각, 변별 offset 50px) +처마변 x∈[357.5,1147.6]·y∈[115.2,636.7] vs baseLine x∈[407.5,1097.6]·y∈[165.2,586.7] (전 변 50px 안쪽). + +| 추녀 끝점 | beforeExt (`:1279`, 연장 전) | afterExt (`:1285`, 연장 후) | +|---|---|---| +| **바깥** 끝점(4코너) | baseLine 코너 — `onRoof=false`, `diffRoof=22.4`, `onBase=true` | **처마 코너로 연장 — `onRoof=true`, `diffRoof=0`** | +| 안쪽·용마루(내부점) | `onRoof=false` (260.7) | `onRoof=false` (260.7) — 내부점이라 정상 | + +예: 추녀 바깥 끝 `(407.5,165.2)`(baseLine) → `(357.5,115.2)`(처마 코너) 로 연장. 4코너 전부 `diffRoof=0`, `onRoof=true`. + +### 9-2. 추녀 연장 메커니즘 (이미 구현됨) +`drawBaselineToRooflineHelpers`(`:685`, 플래그 `DRAW_BASELINE_TO_ROOFLINE_HELPER=true`, `:25`): +- `hipLines` 추출(`:734`) → baseLine 코너에 닿는 hip 만(`CORNER_EPS`, `:773`) → 추녀 진행 방향으로 ray cast(`rayHitSegment`, `:693`) → 처마변 교점까지 hip 좌표를 **연장**(`:861-865`). +- `actualSize` 비율 보정(`:871`) + `__extended`/`attributes.extended` 플래그로 save/load 영속(`:879`). +- 즉 "추녀는 roof.lines 까지 연결되는 선" 이 이미 코드에 있다. SK 골조 자체는 baseLine 기준(동기화 유지) + 추녀 종점만 처마변까지 연장. + +### 9-3. 결론 — split 은 추녀 연장 **다음**에 호출 +- 추녀 종점은 연장 후 처마변 위(`onRoof=true`, `diffRoof=0`)에 정확히 앉으므로, **split/외곽선 innerLine화의 정합은 깨지지 않는다.** +- **유일한 요건**: `splitRoofLinesToInnerLines` 호출을 `createInnerLinesFromSkeleton` 직후가 아니라 **`drawBaselineToRooflineHelpers`(`:1285`) 다음에** 둘 것. §3-2 가 호출 위치를 연장 전으로 잡은 것이 유일한 함정이었음. +- **단순 우진각**: 추녀가 처마 **코너**(변 끝점)에 닿으므로 split 분할점 없음 → 처마 4변 통짜 innerLine(정상). +- **복합 형상**(L자·凸자): 추녀가 처마변 **중간**에 닿으면 그 점에서 분할. 연장 후 종점이 처마변 위이므로 분할점으로 정상 인식. + +### 9-4. 검증 기준 (구현 시) +- split 을 연장 후 호출 → 처마 4변이 innerLine 으로 등록, select+우클릭 편집 가능. +- 복합 형상: 처마변 중간 접점에서 분할, 각 조각 독립 편집. +- offset=0 / offset≠0 양쪽 동작. +- 마루이동/벽이동 후 재그리기에도 유지(연장은 `__extended`/`attributes.extended` 로 영속). +- **임시 계측(`logSkRoofLineAlignment` 정의 + 호출부 3곳 `:1279`/`:1285`/`:1413`) 제거** 후 커밋. diff --git a/src/util/skeleton-utils.js b/src/util/skeleton-utils.js index e283c472..66ea9afb 100644 --- a/src/util/skeleton-utils.js +++ b/src/util/skeleton-utils.js @@ -1,6 +1,6 @@ import { LINE_TYPE, POLYGON_TYPE } from '@/common/common' import { SkeletonBuilder } from '@/lib/skeletons' -import { calcLineActualSize, calcLineActualSize2, calcLinePlaneSize, toGeoJSON } from '@/util/qpolygon-utils' +import { calcLineActualSize, calcLineActualSize2, calcLinePlaneSize, splitRoofLinesToInnerLines, toGeoJSON } from '@/util/qpolygon-utils' import { QLine } from '@/components/fabric/QLine' import { getDegreeByChon } from '@/util/canvas-util' import Big from 'big.js' @@ -1283,6 +1283,13 @@ export const skeletonBuilder = (roofId, canvas, textMode) => { // gap 을 시각화. innerLines 와 별개 collection. 기존 working code (processInBoth 등) 무영향. drawBaselineToRooflineHelpers(roof, canvas, changRoofLinePoints, roofLinePoints) + // [docs §9-3] 추녀 연장 후 처마변(roof.lines)을 split → 편집 가능한 innerLine(hip/roofLine)으로 등록. + // 단순 우진각: 추녀가 처마 코너(변 끝점)에 닿아 분할점 없음 → 처마 4변 통짜. 복합 형상: 중간 접점에서 분할. + splitRoofLinesToInnerLines(roof, roof.innerLines, canvas, textMode) + // 처마선 innerLine 이 외곽선을 전담하므로 roof 원본 외곽(stroke)은 숨긴다. + // (안 숨기면 처마선 삭제 시 roof 의 black stroke 가 노출됨 — usePolygon.js 의 roof 기본 stroke:'black') + roof.set({ stroke: 'transparent' }) + // 캔버스에 스켈레톤 상태 저장 if (!canvas.skeletonStates) { canvas.skeletonStates = {}