Merge branch 'feature/roofline-edit' of https://git.hanasys.jp/qcast3/qcast-front into feature/roofline-edit2

# Conflicts:
#	src/hooks/roofcover/useRoofShapePassivitySetting.js
This commit is contained in:
hyojun.choi 2026-06-15 13:18:21 +09:00
commit 591c828bcf
8 changed files with 236 additions and 49 deletions

View File

@ -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`) 제거** 후 커밋.

View File

@ -40,7 +40,7 @@ class ErrorBoundary extends React.Component {
<p><strong>Timestamp:</strong> {new Date().toISOString()}</p>
<p><strong>Error:</strong> {this.state.error && this.state.error.toString()}</p>
<p><strong>Stack Trace:</strong></p>
<pre>{this.state.errorInfo.componentStack}</pre>
<pre>{this.state.errorInfo?.componentStack}</pre>
</details>
<button
onClick={() => this.setState({ hasError: false, error: null, errorInfo: null })}

View File

@ -558,6 +558,12 @@ export function useCommonUtils() {
if (object.id) {
const group = canvas.getObjects().filter((obj) => obj.id === object.id)
group.forEach((obj) => canvas?.remove(obj))
// QLine 보조선은 lengthText 를 this.text(단수)에 저장해 아래 texts(배열) 제거에 안 걸린다.
// lengthText 는 parentId === line.id 로 연결되므로 parentId 매칭으로 함께 제거 (삭제 시 text 잔존 방지).
canvas
.getObjects()
.filter((obj) => obj.parentId === object.id && obj.name === 'lengthText')
.forEach((obj) => canvas?.remove(obj))
}
if (object.type === 'group') {

View File

@ -39,6 +39,7 @@ export function useRoofShapePassivitySetting(id) {
const isFix = useRef(false)
const isConverted = useRef(false)
const initLines = useRef([])
const removedWalls = useRef([])
const [isLoading, setIsLoading] = useState(false)
const { closePopup } = usePopup()
@ -70,6 +71,7 @@ export function useRoofShapePassivitySetting(id) {
addCanvasMouseEventListener('mouse:down', mouseDown)
const wallLines = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL)
removedWalls.current = wallLines.map((wall) => ({ wall, index: canvas.getObjects().indexOf(wall) }))
canvas?.remove(...wallLines)
const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
@ -221,9 +223,32 @@ export function useRoofShapePassivitySetting(id) {
isConverted.current = true
const roofBases = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)
const exceptObjs = canvas.getObjects().filter((obj) => obj.name !== 'outerLine' && obj.parent?.name !== 'outerLine')
const lines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
if (!isFix.current) {
// 저장 없이 닫을 경우 진입 시점 스냅샷으로 롤백 — 캔버스를 진입 전 상태 그대로 되돌린다
lines.forEach((line, idx) => {
const init = initLines.current[idx]
if (!init) {
return
}
init.visible ? showLine(line) : hideLine(line)
line.set({ stroke: init.stroke, strokeWidth: init.strokeWidth, selectable: init.selectable })
line.attributes = init.attributes
})
// 진입 시 삭제했던 WALL 폴리곤 원본을 원래 z-순서 그대로 재삽입
removedWalls.current.forEach(({ wall, index }) => {
canvas.insertAt(wall, index)
})
removedWalls.current = []
canvas.renderAll()
return
}
const exceptObjs = canvas.getObjects().filter((obj) => obj.name !== 'outerLine' && obj.parent?.name !== 'outerLine')
let checkedAllSetting = true
lines.forEach((line) => {
@ -241,29 +266,26 @@ export function useRoofShapePassivitySetting(id) {
canvas.remove(obj)
})
// 저장 시에는 자동 설정과 동일하게 외벽선을 타입별 색상으로 표시
lines.forEach((line) => {
hideLine(line)
let stroke
if (line.attributes?.type === LINE_TYPE.WALLLINE.EAVES || line.attributes?.type === LINE_TYPE.WALLLINE.HIPANDGABLE) {
stroke = '#45CD7D'
} else if (line.attributes?.type === LINE_TYPE.WALLLINE.GABLE || line.attributes?.type === LINE_TYPE.WALLLINE.JERKINHEAD) {
stroke = '#3FBAE6'
} else {
stroke = '#000000'
}
line.set({ stroke, strokeWidth: 4, selectable: false, visible: true })
})
let wall
if (isFix.current) {
wall = addPolygonByLines(lines, { name: POLYGON_TYPE.WALL, fill: 'transparent', stroke: 'black' })
} else {
// 그냥 닫을 경우 처리
wall = addPolygonByLines([...initLines.current], {
name: POLYGON_TYPE.WALL,
fill: 'transparent',
stroke: 'black',
})
lines.forEach((line, idx) => {
line.attributes = initLines.current[idx].attributes
})
}
const wall = addPolygonByLines(lines, { name: POLYGON_TYPE.WALL, fill: 'transparent', stroke: 'black' })
wall.lines = [...lines]
// 기존 그려진 지붕이 없다면
// 완료 한 경우에는 지붕까지 그려줌
addPitchTextsByOuterLines()
const roof = drawRoofPolygon(wall)
addLengthText(roof)
if (isFix.current) {
// 완료 한 경우에는 지붕까지 그려줌
@ -273,6 +295,12 @@ export function useRoofShapePassivitySetting(id) {
const roof = drawRoofPolygon(wall)
addLengthText(roof)
}
// 외벽선은 WALL/ROOF 폴리곤 바로 위에 배치 — 맨 앞(bringToFront)으로 올리면
// offset 0인 변에서 같은 좌표에 그려진 보조선의 클릭을 가로채 선택을 막는다
const polygonTopIdx = canvas
.getObjects()
.reduce((top, obj, idx) => (obj.name === POLYGON_TYPE.WALL || obj.name === POLYGON_TYPE.ROOF ? idx : top), -1)
lines.forEach((line) => canvas.moveTo(line, polygonTopIdx))
canvas.renderAll()
}

View File

@ -55,6 +55,11 @@ export function useRoofShapeSetting(id) {
const isFixRef = useRef(false)
// 저장 없이 닫을 때 롤백할 진입 시점 스냅샷
const initLinesRef = useRef([])
const removedWallsRef = useRef([])
const pitchTextLineIdsRef = useRef(new Set())
const pitchRef = useRef(null)
const shedPitchRef = useRef(null)
const jerkinHeadPitchRef = useRef(null)
@ -104,14 +109,46 @@ export function useRoofShapeSetting(id) {
closePopup(id)
return
}
return () => {
if (!isFixRef.current) {
return
}
initLinesRef.current = outerLines.map((line) => ({ ...line }))
pitchTextLineIdsRef.current = new Set(
canvas
.getObjects()
.filter((obj) => obj.name === 'pitchText')
.map((obj) => obj.parentId),
)
return () => {
const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
const pitchTexts = canvas.getObjects().filter((obj) => obj.name === 'pitchText')
if (!isFixRef.current) {
// 저장 없이 닫을 경우 진입 시점 스냅샷으로 롤백 — 캔버스를 진입 전 상태 그대로 되돌린다
canvas.remove(...pitchTexts)
outerLines.forEach((line, idx) => {
const init = initLinesRef.current[idx]
if (!init) {
return
}
init.visible ? showLine(line) : hideLine(line)
line.set({ stroke: init.stroke, strokeWidth: init.strokeWidth, selectable: init.selectable })
line.attributes = init.attributes
if (pitchTextLineIdsRef.current.has(line.id)) {
addPitchText(line)
}
})
// 변별로 설정 진입 시 삭제했던 WALL 폴리곤 원본을 원래 z-순서 그대로 재삽입
removedWallsRef.current.forEach(({ wall, index }) => {
canvas.insertAt(wall, index)
})
removedWallsRef.current = []
canvas.discardActiveObject()
canvas.renderAll()
return
}
canvas.remove(...pitchTexts)
outerLines.forEach((line) => {
let stroke, strokeWidth
@ -130,12 +167,20 @@ export function useRoofShapeSetting(id) {
line.set({
stroke,
strokeWidth,
selectable: false,
visible: true,
})
addPitchText(line)
line.bringToFront()
}
})
// 외벽선은 WALL/ROOF 폴리곤 바로 위에 배치 — 맨 앞(bringToFront)으로 올리면
// offset 0인 변에서 같은 좌표에 그려진 보조선의 클릭을 가로채 선택을 막는다
const polygonTopIdx = canvas
.getObjects()
.reduce((top, obj, idx) => (obj.name === POLYGON_TYPE.WALL || obj.name === POLYGON_TYPE.ROOF ? idx : top), -1)
outerLines.forEach((line) => canvas.moveTo(line, polygonTopIdx))
canvas.renderAll()
}
}, [])
@ -161,10 +206,15 @@ export function useRoofShapeSetting(id) {
useEffect(() => {
if (shapeNum === 4) {
canvas?.remove(canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL))
const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL)
if (wall && removedWallsRef.current.length === 0) {
removedWallsRef.current = [{ wall, index: canvas.getObjects().indexOf(wall) }]
}
canvas?.remove(wall)
const outerLines = canvas.getObjects().filter((obj) => obj.name === 'outerLine')
outerLines.forEach((line) => {
showLine(line)
line.set({ selectable: true })
line.bringToFront()
})

View File

@ -148,7 +148,7 @@ export const useLine = () => {
// 기존 pitchText가 있으면 삭제
if (pitchText.length > 0) {
canvas.remove(pitchText)
canvas.remove(...pitchText)
}
}

View File

@ -1628,6 +1628,10 @@ export const drawGableRoof = (roofId, canvas, textMode) => {
drawRoofPlane(backward)
})
roof.innerLines.push(...ridgeLines, ...innerLines)
// 처마선 innerLine 이 외곽선/치수를 전담하므로 roof 원본 외곽(stroke) 숨김 + 지붕선 치수(roof lengthText) 제거.
// 외벽선(wall) 치수는 유지. (박공은 split 공용 함수를 안 거치므로 여기서 직접 처리)
roof.set({ stroke: 'transparent' })
removeRoofLengthText(roof, canvas)
canvas
.getObjects()
.filter((obj) => obj.name === 'check')
@ -1719,6 +1723,16 @@ export const drawShedRoof = (roofId, canvas, textMode) => {
const maxLine = pitchSizeLines.reduce((prev, current) => (prev.length > current.length ? prev : current), pitchSizeLines[0])
canvas.add(maxLine)
// [docs §8] 한쪽흐름 외곽 처마선 — 내부 보조선이 없어 각 처마변이 통짜 innerLine 으로 그려진다.
// 경사변(gable)은 평면길이≠실제길이 라서 degreeOf 로 shedDegree 를 주입 → drawHipLine(plane/cos) 으로 보정.
// roof 원본 외곽(stroke) 숨김은 splitRoofLinesToInnerLines 가 담당.
roof.innerLines = roof.innerLines || []
splitRoofLinesToInnerLines(roof, roof.innerLines, canvas, textMode, {
degreeOf: (parentLine) => (parentLine.attributes?.type === LINE_TYPE.WALLLINE.GABLE ? shedDegree : 0),
})
// 지붕선 치수(roof lengthText) 제거는 split 내부 removeRoofLengthText 가 담당. 외벽선(wall) 치수는 유지.
canvas.renderAll()
}
@ -5429,7 +5443,52 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
//추가된 하단 지붕 라인 innerLines에 추가.
innerLines.push(...downRoofLines)
//지붕선에 따라 라인추가 작업 처리.
//지붕선에 따라 라인추가 작업 처리. (split 공용 함수 — docs §3-1)
splitRoofLinesToInnerLines(roof, innerLines, canvas, textMode)
//지붕 innerLines에 추가.
roof.innerLines = innerLines
canvas
.getObjects()
.filter((object) => object.name === 'check')
.forEach((object) => canvas.remove(object))
canvas.renderAll()
}
/**
* 지붕선(roof.lines) 원본 길이 text(lengthText) 제거한다.
* 지붕선 치수는 위에 그려진 보조선(처마선 innerLine) 치수로 대체되므로 roof.lines 자체 치수는 숨긴다.
* 외벽선(wall) 치수는 유지한다. (변별/박공/용마루/한쪽흐름 공통)
*/
export const removeRoofLengthText = (roof, canvas) => {
canvas
?.getObjects()
.filter((o) => o.name === 'lengthText' && o.parentId === roof.id)
.forEach((o) => canvas.remove(o))
}
/**
* roof.lines(처마 외곽선) innerLine 끝점 기준으로 split 편집 가능한 innerLine(hip/roofLine)으로 만들고,
* 처마선 innerLine 외곽선을 전담하므로 roof 원본 외곽선(stroke) 숨긴다(transparent).
* 변별/용마루 경로 공용. innerLines 결과를 in-place push 하고 반환한다.
* (용마루 경로는 추녀 연장 호출해야 종점이 처마변 위에 있어 분할점이 잡힌다 docs §9)
* @param {fabric.Object} roof - 대상 지붕 (roof.lines 사용)
* @param {Array<QLine>} innerLines - 내부선 배열 (split 결과가 여기에 in-place 추가됨)
* @param {fabric.Canvas} canvas
* @param {string} textMode
* @param {{degreeOf?: (parentLine) => number}} [opts] - degreeOf: 부모 외곽변 경사각().
* 한쪽흐름 경사변(gable)처럼 평면길이실제길이인 변을 통짜로 그릴 drawHipLine 으로 실제길이 보정(plane/cos) docs §8.
* 미전달 기존 동작(분할점 인접 pitch 기반).
* @returns {Array<QLine>} innerLines
*/
export const splitRoofLinesToInnerLines = (roof, innerLines, canvas, textMode, opts = {}) => {
const { degreeOf } = opts
// 분할점이 처마변 끝점(코너)과 이 거리(px) 이내면 분할에서 제외.
// SK 추녀 연장이 처마 코너에 닿을 때 ray cast 부동소수 오차(관측 0.02px)로 코너와 미세하게
// 어긋나면, 기존 almostEqual(1e-10)은 "끝점 아님"으로 오판 → 0 길이 조각 생성. END_EPS 로 흡수.
// 변별 경로의 진짜 분할점은 끝점에서 수백 px 떨어져 있어 영향 없음.
const END_EPS = 1.0
const innerLinesPoints = []
innerLines.forEach((line) => {
const hasCoord1 = innerLinesPoints.find((p) => p.x === line.x1 && p.y === line.y1)
@ -5464,17 +5523,15 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
if (hasOverlapLine) return
innerLinesPoints.forEach((point) => {
if (
isPointOnLineNew(currentLine, point) &&
!(almostEqual(currentLine.x1, point.x) && almostEqual(currentLine.y1, point.y)) &&
!(almostEqual(currentLine.x2, point.x) && almostEqual(currentLine.y2, point.y))
) {
const distance = Math.sqrt((point.x - currentLine.x1) ** 2 + (point.y - currentLine.y1) ** 2)
splitPoint.push({ point, distance })
const distToStart = Math.hypot(point.x - currentLine.x1, point.y - currentLine.y1)
const distToEnd = Math.hypot(point.x - currentLine.x2, point.y - currentLine.y2)
// 변 위 점이면서 양 끝점(코너)에서 END_EPS 보다 멀 때만 진짜 분할점으로 채택.
if (isPointOnLineNew(currentLine, point) && distToStart > END_EPS && distToEnd > END_EPS) {
splitPoint.push({ point, distance: distToStart })
}
})
if (splitPoint.length > 0) {
splitPoint.sort((a, b) => a[1] - b[1])
splitPoint.sort((a, b) => a.distance - b.distance)
let startPoint = { x: currentLine.x1, y: currentLine.y1 }
for (let i = 0; i < splitPoint.length; i++) {
const point = splitPoint[i].point
@ -5491,18 +5548,22 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
innerLines.push(drawHipLine([startPoint.x, startPoint.y, currentLine.x2, currentLine.y2], canvas, roof, textMode, nextDegree, nextDegree))
}
} else {
innerLines.push(drawRoofLine([currentLine.x1, currentLine.y1, currentLine.x2, currentLine.y2], canvas, roof, textMode))
// 분할점 없는 통짜 변. degreeOf 가 주어지면(한쪽흐름) 경사변은 drawHipLine(plane/cos) 으로 실제길이 보정.
const deg = degreeOf ? degreeOf(currentLine) : 0
if (deg > 0) {
innerLines.push(drawHipLine([currentLine.x1, currentLine.y1, currentLine.x2, currentLine.y2], canvas, roof, textMode, deg, deg))
} else {
innerLines.push(drawRoofLine([currentLine.x1, currentLine.y1, currentLine.x2, currentLine.y2], canvas, roof, textMode))
}
}
})
//지붕 innerLines에 추가.
roof.innerLines = innerLines
// 처마선 innerLine 이 외곽선/치수를 전담하므로 roof 원본 외곽(stroke) 숨김 + 지붕선 치수(roof lengthText) 제거.
// (안 숨기면 처마선 삭제 시 roof 기본 stroke:'black'(usePolygon.js) 노출. 외벽선 wall 치수는 유지)
roof.set({ stroke: 'transparent' })
removeRoofLengthText(roof, canvas)
canvas
.getObjects()
.filter((object) => object.name === 'check')
.forEach((object) => canvas.remove(object))
canvas.renderAll()
return innerLines
}
/**

View File

@ -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,10 @@ 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)
// 캔버스에 스켈레톤 상태 저장
if (!canvas.skeletonStates) {
canvas.skeletonStates = {}