416 lines
19 KiB
JavaScript
416 lines
19 KiB
JavaScript
// [2240 KERAB-OFFSET-SURGICAL 2026-05-27] 케라바 forward/revert 시 出幅(offset) 변경분만
|
|
// target 의 matchingRoofLine + 인접 2 corner roof.points + corner 에 닿은 inner line 끝점
|
|
// + lengthText 라벨을 surgical 갱신. SK 재실행 없이 다른 벽의 케라바 패턴 보존.
|
|
//
|
|
// 고객 회귀 검증을 위해 본 로직 전체를 useEavesGableEdit.js 에서 분리.
|
|
// 호출 ON/OFF 는 useEavesGableEdit.js 상단 `ENABLE_KERAB_OFFSET_SURGICAL` 상수로 토글한다.
|
|
|
|
import { fabric } from 'fabric'
|
|
import { POLYGON_TYPE } from '@/common/common'
|
|
import { calcLinePlaneSize } from '@/util/qpolygon-utils'
|
|
import { logger } from '@/util/logger'
|
|
|
|
const lineLineIntersection = (p1, p2, p3, p4) => {
|
|
const d = (p1.x - p2.x) * (p3.y - p4.y) - (p1.y - p2.y) * (p3.x - p4.x)
|
|
if (Math.abs(d) < 1e-6) return null
|
|
const t = ((p1.x - p3.x) * (p3.y - p4.y) - (p1.y - p3.y) * (p3.x - p4.x)) / d
|
|
return { x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y) }
|
|
}
|
|
|
|
/**
|
|
* @param canvas fabric canvas
|
|
* @param target outerLine fabric 객체 (target.attributes 는 OLD 상태로 호출되어야 함)
|
|
* @param newOffset 새 출폭 (canvas 단위)
|
|
* @returns true=적용됨 / false=조건 미달 또는 변경 없음
|
|
*/
|
|
export const applyKerabOffsetSurgical = (canvas, target, newOffset) => {
|
|
const roof = canvas
|
|
.getObjects()
|
|
.find((o) => o.name === POLYGON_TYPE.ROOF && !o.isFixed && o.id === target.attributes?.roofId)
|
|
if (!roof || !Array.isArray(roof.lines) || !Array.isArray(roof.points)) return false
|
|
const idx = roof.lines.findIndex((rl) => rl && rl.attributes?.wallLine === target.id)
|
|
if (idx < 0) return false
|
|
const matchingRL = roof.lines[idx]
|
|
const oldOffset = matchingRL.attributes?.offset ?? 0
|
|
if (Math.abs(newOffset - oldOffset) < 1e-3) return false
|
|
|
|
const N = roof.lines.length
|
|
const prevRL = roof.lines[(idx - 1 + N) % N]
|
|
const nextRL = roof.lines[(idx + 1) % N]
|
|
|
|
// target wall edge 의 outward unit normal — matchingRL 현재 위치로 sign 추정 (oldOffset=0 시 centroid 폴백).
|
|
const tdx = target.x2 - target.x1
|
|
const tdy = target.y2 - target.y1
|
|
const tL = Math.hypot(tdx, tdy) || 1
|
|
let nx = -tdy / tL
|
|
let ny = tdx / tL
|
|
const tMid = { x: (target.x1 + target.x2) / 2, y: (target.y1 + target.y2) / 2 }
|
|
if (Math.abs(oldOffset) > 1e-6) {
|
|
const mMid = { x: (matchingRL.x1 + matchingRL.x2) / 2, y: (matchingRL.y1 + matchingRL.y2) / 2 }
|
|
if ((mMid.x - tMid.x) * nx + (mMid.y - tMid.y) * ny < 0) {
|
|
nx = -nx
|
|
ny = -ny
|
|
}
|
|
} else {
|
|
let cx = 0
|
|
let cy = 0
|
|
for (const p of roof.points) {
|
|
cx += p.x
|
|
cy += p.y
|
|
}
|
|
cx /= roof.points.length
|
|
cy /= roof.points.length
|
|
if ((tMid.x - cx) * nx + (tMid.y - cy) * ny < 0) {
|
|
nx = -nx
|
|
ny = -ny
|
|
}
|
|
}
|
|
|
|
// 새 matchingRL axis 와 prev/nextRL 무한확장 교점.
|
|
const newAxisP1 = { x: target.x1 + nx * newOffset, y: target.y1 + ny * newOffset }
|
|
const newAxisP2 = { x: target.x2 + nx * newOffset, y: target.y2 + ny * newOffset }
|
|
const newCorner1 = lineLineIntersection(
|
|
{ x: prevRL.x1, y: prevRL.y1 },
|
|
{ x: prevRL.x2, y: prevRL.y2 },
|
|
newAxisP1,
|
|
newAxisP2,
|
|
)
|
|
const newCorner2 = lineLineIntersection(
|
|
newAxisP1,
|
|
newAxisP2,
|
|
{ x: nextRL.x1, y: nextRL.y1 },
|
|
{ x: nextRL.x2, y: nextRL.y2 },
|
|
)
|
|
if (!newCorner1 || !newCorner2) {
|
|
logger.log('[KERAB-OFFSET-SURGICAL] intersect failed — skip surgical update')
|
|
return false
|
|
}
|
|
|
|
const oldCorner1 = { x: roof.points[idx].x, y: roof.points[idx].y }
|
|
const oldCorner2 = { x: roof.points[(idx + 1) % N].x, y: roof.points[(idx + 1) % N].y }
|
|
|
|
matchingRL.set({ x1: newCorner1.x, y1: newCorner1.y, x2: newCorner2.x, y2: newCorner2.y })
|
|
prevRL.set({ x2: newCorner1.x, y2: newCorner1.y })
|
|
nextRL.set({ x1: newCorner2.x, y1: newCorner2.y })
|
|
|
|
const newSize = calcLinePlaneSize({ x1: newCorner1.x, y1: newCorner1.y, x2: newCorner2.x, y2: newCorner2.y })
|
|
matchingRL.attributes = {
|
|
...matchingRL.attributes,
|
|
offset: newOffset,
|
|
planeSize: newSize,
|
|
actualSize: newSize,
|
|
}
|
|
const prevSize = calcLinePlaneSize({ x1: prevRL.x1, y1: prevRL.y1, x2: prevRL.x2, y2: prevRL.y2 })
|
|
prevRL.attributes = { ...prevRL.attributes, planeSize: prevSize, actualSize: prevSize }
|
|
const nextSize = calcLinePlaneSize({ x1: nextRL.x1, y1: nextRL.y1, x2: nextRL.x2, y2: nextRL.y2 })
|
|
nextRL.attributes = { ...nextRL.attributes, planeSize: nextSize, actualSize: nextSize }
|
|
|
|
// [KERAB-OFFSET-SHRINK-TRIM 2026-06-01] 도메인 룰:
|
|
// - 기본: skLine + ext 원본 좌표 보존.
|
|
// - 원본 끝점이 새 roofLine 변 바깥(wall 쪽)에 있으면 새 roofLine 변과의 교점으로 절삭.
|
|
// - 원본 끝점이 새 roofLine 변 안쪽으로 복귀하면 원본 좌표로 복원 (출폭 다시 늘린 케이스).
|
|
// 원본 좌표는 il.__shrinkOrig 에 보관 — 첫 절삭 시점에 백업, 양 끝 모두 안쪽으로 복귀 시 삭제.
|
|
// 매 surgical 호출마다 원본 기반으로 재계산하므로 출폭 증감 무관 idempotent.
|
|
{
|
|
const newAxisMid = { x: (newCorner1.x + newCorner2.x) / 2, y: (newCorner1.y + newCorner2.y) / 2 }
|
|
const OUTSIDE_TOL = 0.5
|
|
const segOk = (ip) => {
|
|
const minX = Math.min(newCorner1.x, newCorner2.x) - OUTSIDE_TOL
|
|
const maxX = Math.max(newCorner1.x, newCorner2.x) + OUTSIDE_TOL
|
|
const minY = Math.min(newCorner1.y, newCorner2.y) - OUTSIDE_TOL
|
|
const maxY = Math.max(newCorner1.y, newCorner2.y) + OUTSIDE_TOL
|
|
return ip.x >= minX && ip.x <= maxX && ip.y >= minY && ip.y <= maxY
|
|
}
|
|
// [KERAB-PATTERN-CORNER-SNAP 2026-06-01] 케라바 패턴 라인(kLine/ExtRidge/Hip/ExtHip)은
|
|
// roofLine corner 변경에 따라 끝점도 같이 이동. corner 일치뿐 아니라 옛 segment 위의
|
|
// 중간 점(예: kLine 끝점이 옛 roofLine 변 위)도 새 segment 의 동일 t 비율 점으로 매핑.
|
|
const isKerabPatternLine = (il) =>
|
|
il &&
|
|
(il.lineName === 'kerabPatternRidge' ||
|
|
il.lineName === 'kerabPatternExtRidge' ||
|
|
il.lineName === 'kerabPatternHip' ||
|
|
il.lineName === 'kerabPatternExtHip' ||
|
|
il.lineName === 'kerabPatternValleyExt' ||
|
|
il.lineName === 'kerabValleyOverlapLine')
|
|
const oldSegDx = oldCorner2.x - oldCorner1.x
|
|
const oldSegDy = oldCorner2.y - oldCorner1.y
|
|
const oldSegLen2 = oldSegDx * oldSegDx + oldSegDy * oldSegDy || 1
|
|
const newSegDx = newCorner2.x - newCorner1.x
|
|
const newSegDy = newCorner2.y - newCorner1.y
|
|
const mapToNewSeg = (pt) => {
|
|
const dx = pt.x - oldCorner1.x
|
|
const dy = pt.y - oldCorner1.y
|
|
const t = (dx * oldSegDx + dy * oldSegDy) / oldSegLen2
|
|
const projX = oldCorner1.x + t * oldSegDx
|
|
const projY = oldCorner1.y + t * oldSegDy
|
|
const perpD = Math.hypot(pt.x - projX, pt.y - projY)
|
|
if (perpD > 0.5) return null
|
|
if (t < -0.05 || t > 1.05) return null
|
|
return { x: newCorner1.x + t * newSegDx, y: newCorner1.y + t * newSegDy }
|
|
}
|
|
for (const il of roof.innerLines || []) {
|
|
if (!il) continue
|
|
|
|
// 케라바 패턴 라인: 옛 roofLine segment 위 끝점 → 새 segment 의 동일 t 점.
|
|
// 한 끝만 segment 위(vExt 등 self-extension)일 때는 다른 끝도 같은 변위로 평행 이동 →
|
|
// 직각/형상 보존. 절삭/복원 흐름 skip.
|
|
if (isKerabPatternLine(il)) {
|
|
const oldX1 = il.x1
|
|
const oldY1 = il.y1
|
|
const oldX2 = il.x2
|
|
const oldY2 = il.y2
|
|
const np1 = mapToNewSeg({ x: il.x1, y: il.y1 })
|
|
const np2 = mapToNewSeg({ x: il.x2, y: il.y2 })
|
|
if (np1 && np2) {
|
|
il.set({ x1: np1.x, y1: np1.y, x2: np2.x, y2: np2.y })
|
|
} else if (np1 && !np2) {
|
|
const dx = np1.x - il.x1
|
|
const dy = np1.y - il.y1
|
|
il.set({ x1: np1.x, y1: np1.y, x2: il.x2 + dx, y2: il.y2 + dy })
|
|
} else if (!np1 && np2) {
|
|
const dx = np2.x - il.x2
|
|
const dy = np2.y - il.y2
|
|
il.set({ x1: il.x1 + dx, y1: il.y1 + dy, x2: np2.x, y2: np2.y })
|
|
}
|
|
if (typeof il.setCoords === 'function') il.setCoords()
|
|
if (np1 || np2) {
|
|
logger.log(
|
|
'[KERAB-PATTERN-CORNER-SNAP] mapped ' +
|
|
JSON.stringify({ lineName: il.lineName, np1, np2, newPts: { x1: il.x1, y1: il.y1, x2: il.x2, y2: il.y2 } }),
|
|
)
|
|
// [KERAB-PATTERN-CASCADE 2026-06-01] vExt 등 평행 이동 시 옛 끝점에 닿아있던
|
|
// 다른 innerLine 끝점도 같은 변위로 평행 이동. RG-1 의 valley-trim 결과 끝점이
|
|
// vExt 끝점과 분리되어 split graph 의 closed path 안 만들어지는 문제 해소.
|
|
// cascade: vExt 옛 segment 위에 끝점이 있는 다른 innerLine 도 같은 변위로 평행 이동.
|
|
// 끝점-끝점 일치 외에 segment 위 중간 점(예: kLine 끝점이 vExt segment 위)도 매칭.
|
|
// 케라바 패턴 라인 가드 제거 — 자체 매핑된 라인은 이미 새 좌표라 옛 segment 위 X → 자동 skip.
|
|
const dxVExt = il.x1 - oldX1
|
|
const dyVExt = il.y1 - oldY1
|
|
const pointOnOldSeg = (px, py) => {
|
|
const sdx = oldX2 - oldX1
|
|
const sdy = oldY2 - oldY1
|
|
const slen2 = sdx * sdx + sdy * sdy
|
|
if (slen2 < 1e-6) return Math.hypot(px - oldX1, py - oldY1) < 1.0
|
|
const t = ((px - oldX1) * sdx + (py - oldY1) * sdy) / slen2
|
|
if (t < -0.02 || t > 1.02) return false
|
|
const projX = oldX1 + t * sdx
|
|
const projY = oldY1 + t * sdy
|
|
return Math.hypot(px - projX, py - projY) < 1.0
|
|
}
|
|
if (Math.hypot(dxVExt, dyVExt) > 0.01) {
|
|
// cascade 대상: roof.innerLines + canvas 의 kerabValleyOverlapLine (innerLines 미포함).
|
|
// overlap 보조선들은 vExt 의 끝점과 90도로 만나므로 vExt 이동에 같이 따라가야 정합.
|
|
const overlapInCanvas = (canvas.getObjects() || []).filter(
|
|
(o) =>
|
|
o &&
|
|
o.lineName === 'kerabValleyOverlapLine' &&
|
|
(o.roofId === roof.id || o.attributes?.roofId === roof.id),
|
|
)
|
|
const cascadeTargets = [...(roof.innerLines || []), ...overlapInCanvas]
|
|
for (const other of cascadeTargets) {
|
|
if (!other || other === il) continue
|
|
let moved = false
|
|
const hit1 = pointOnOldSeg(other.x1, other.y1)
|
|
const hit2 = pointOnOldSeg(other.x2, other.y2)
|
|
// [KERAB-VALLEY-EXT-PARALLEL 2026-06-05] vExt(골짜기 확장라인)는 self-extension
|
|
// 수직/수평 라인이라 한 끝만 cascade 로 끌면 대각선이 된다 (사용자 룰: 대각선은 hip뿐).
|
|
// split 된 vExt 세그먼트는 한 끝만 옛 segment 에 닿아도 양 끝을 같은 변위로 평행이동.
|
|
if (other.lineName === 'kerabPatternValleyExt' && (hit1 || hit2)) {
|
|
other.set({
|
|
x1: other.x1 + dxVExt,
|
|
y1: other.y1 + dyVExt,
|
|
x2: other.x2 + dxVExt,
|
|
y2: other.y2 + dyVExt,
|
|
})
|
|
moved = true
|
|
} else {
|
|
if (hit1) {
|
|
other.set({ x1: other.x1 + dxVExt, y1: other.y1 + dyVExt })
|
|
moved = true
|
|
}
|
|
if (hit2) {
|
|
other.set({ x2: other.x2 + dxVExt, y2: other.y2 + dyVExt })
|
|
moved = true
|
|
}
|
|
}
|
|
if (moved) {
|
|
if (typeof other.setCoords === 'function') other.setCoords()
|
|
logger.log(
|
|
'[KERAB-PATTERN-CASCADE] moved ' +
|
|
JSON.stringify({
|
|
lineName: other.lineName,
|
|
name: other.name,
|
|
dx: dxVExt,
|
|
dy: dyVExt,
|
|
newPts: { x1: other.x1, y1: other.y1, x2: other.x2, y2: other.y2 },
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
const orig = il.__shrinkOrig || { x1: il.x1, y1: il.y1, x2: il.x2, y2: il.y2 }
|
|
const orig1 = { x: orig.x1, y: orig.y1 }
|
|
const orig2 = { x: orig.x2, y: orig.y2 }
|
|
|
|
// [KERAB-OFFSET-CORNER-SHORTCUT 2026-06-01] orig 끝점이 옛 corner 와 일치하면 새 corner 로 직접 snap.
|
|
// 일반 절삭 흐름은 il segment 와 새 roofLine 변의 lineLineIntersection 으로 ip 계산하는데,
|
|
// 끝점이 옛 corner 위에 있으면 ip 가 새 corner segment 밖으로 떨어져 segOk 가 reject → 절삭 실패.
|
|
// 그 결과 c1·c2 비대칭으로 kLine 대각선 변형됨.
|
|
const CORNER_SNAP_TOL_TRIM = 0.5
|
|
let cornerSnapped = false
|
|
const trySnap = (epx, epy, which) => {
|
|
if (Math.hypot(epx - oldCorner1.x, epy - oldCorner1.y) < CORNER_SNAP_TOL_TRIM) {
|
|
if (!il.__shrinkOrig) il.__shrinkOrig = { x1: orig.x1, y1: orig.y1, x2: orig.x2, y2: orig.y2 }
|
|
if (which === 1) il.set({ x1: newCorner1.x, y1: newCorner1.y })
|
|
else il.set({ x2: newCorner1.x, y2: newCorner1.y })
|
|
cornerSnapped = true
|
|
return true
|
|
}
|
|
if (Math.hypot(epx - oldCorner2.x, epy - oldCorner2.y) < CORNER_SNAP_TOL_TRIM) {
|
|
if (!il.__shrinkOrig) il.__shrinkOrig = { x1: orig.x1, y1: orig.y1, x2: orig.x2, y2: orig.y2 }
|
|
if (which === 1) il.set({ x1: newCorner2.x, y1: newCorner2.y })
|
|
else il.set({ x2: newCorner2.x, y2: newCorner2.y })
|
|
cornerSnapped = true
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
trySnap(orig.x1, orig.y1, 1)
|
|
trySnap(orig.x2, orig.y2, 2)
|
|
if (cornerSnapped) {
|
|
if (typeof il.setCoords === 'function') il.setCoords()
|
|
logger.log(
|
|
'[KERAB-OFFSET-CORNER-SHORTCUT] snapped ' +
|
|
JSON.stringify({ lineName: il.lineName, x1: il.x1, y1: il.y1, x2: il.x2, y2: il.y2 }),
|
|
)
|
|
continue
|
|
}
|
|
|
|
const d1 = (orig1.x - newAxisMid.x) * nx + (orig1.y - newAxisMid.y) * ny
|
|
const d2 = (orig2.x - newAxisMid.x) * nx + (orig2.y - newAxisMid.y) * ny
|
|
|
|
// 양 끝 모두 새 polygon 안쪽 → 원본 복원 + 백업 삭제
|
|
if (d1 <= OUTSIDE_TOL && d2 <= OUTSIDE_TOL) {
|
|
if (il.__shrinkOrig) {
|
|
il.set({ x1: orig.x1, y1: orig.y1, x2: orig.x2, y2: orig.y2 })
|
|
if (typeof il.setCoords === 'function') il.setCoords()
|
|
logger.log(
|
|
'[KERAB-OFFSET-SHRINK-TRIM] restored ' +
|
|
JSON.stringify({ lineName: il.lineName, x1: orig.x1, y1: orig.y1, x2: orig.x2, y2: orig.y2 }),
|
|
)
|
|
delete il.__shrinkOrig
|
|
}
|
|
continue
|
|
}
|
|
|
|
// 바깥 끝점 절삭 (원본 segment 와 새 roofLine 변의 교점)
|
|
let nx1 = orig.x1
|
|
let ny1 = orig.y1
|
|
let nx2 = orig.x2
|
|
let ny2 = orig.y2
|
|
let trimmed = false
|
|
if (d1 > OUTSIDE_TOL) {
|
|
const ip = lineLineIntersection(orig1, orig2, newCorner1, newCorner2)
|
|
if (ip && segOk(ip)) {
|
|
nx1 = ip.x
|
|
ny1 = ip.y
|
|
trimmed = true
|
|
}
|
|
}
|
|
if (d2 > OUTSIDE_TOL) {
|
|
const ip = lineLineIntersection(orig1, orig2, newCorner1, newCorner2)
|
|
if (ip && segOk(ip)) {
|
|
nx2 = ip.x
|
|
ny2 = ip.y
|
|
trimmed = true
|
|
}
|
|
}
|
|
if (!trimmed) continue
|
|
if (!il.__shrinkOrig) {
|
|
il.__shrinkOrig = { x1: orig.x1, y1: orig.y1, x2: orig.x2, y2: orig.y2 }
|
|
}
|
|
il.set({ x1: nx1, y1: ny1, x2: nx2, y2: ny2 })
|
|
if (typeof il.setCoords === 'function') il.setCoords()
|
|
logger.log(
|
|
'[KERAB-OFFSET-SHRINK-TRIM] trimmed ' +
|
|
JSON.stringify({ lineName: il.lineName, orig, newPts: { x1: nx1, y1: ny1, x2: nx2, y2: ny2 } }),
|
|
)
|
|
}
|
|
}
|
|
|
|
// points 는 새 배열로 set 해야 fabric 의 dirty 감지가 동작.
|
|
// [KERAB-OFFSET-SURGICAL 2026-05-29] 출폭 증가 시 새 corner 가 polygon bbox 밖에 있으면
|
|
// 외곽선이 안 그려짐. _setPositionDimensions 로 width/height/pathOffset 재계산 + 앵커
|
|
// 절대좌표 보존(setPositionByOrigin) 으로 polygon path 가 새 영역까지 다시 그려지게 강제.
|
|
const newPoints = roof.points.map((p, i) => {
|
|
if (i === idx) return { x: newCorner1.x, y: newCorner1.y }
|
|
if (i === (idx + 1) % N) return { x: newCorner2.x, y: newCorner2.y }
|
|
return { x: p.x, y: p.y }
|
|
})
|
|
let absolutePoint = null
|
|
let anchorIdx = 0
|
|
// 변경 대상이 아닌 첫 인덱스를 앵커로 — pathOffset 갱신 후 그 점 절대 좌표 보존.
|
|
for (let i = 0; i < roof.points.length; i++) {
|
|
if (i !== idx && i !== (idx + 1) % N) {
|
|
anchorIdx = i
|
|
break
|
|
}
|
|
}
|
|
try {
|
|
if (typeof roof.calcTransformMatrix === 'function' && roof.pathOffset) {
|
|
const oldLocal = {
|
|
x: roof.points[anchorIdx].x - roof.pathOffset.x,
|
|
y: roof.points[anchorIdx].y - roof.pathOffset.y,
|
|
}
|
|
absolutePoint = fabric.util.transformPoint(oldLocal, roof.calcTransformMatrix())
|
|
}
|
|
} catch (e) {
|
|
absolutePoint = null
|
|
}
|
|
|
|
roof.points = newPoints
|
|
roof.set({ points: newPoints, dirty: true })
|
|
if (typeof roof._setPositionDimensions === 'function') roof._setPositionDimensions({})
|
|
if (absolutePoint && typeof roof.setPositionByOrigin === 'function') {
|
|
const strokeW = roof.strokeUniform ? roof.strokeWidth / Math.max(roof.scaleX || 1, 1e-9) : roof.strokeWidth
|
|
const baseW = (roof.width || 0) + strokeW
|
|
const baseH = (roof.height || 0) + (roof.strokeUniform ? roof.strokeWidth / Math.max(roof.scaleY || 1, 1e-9) : roof.strokeWidth)
|
|
const newX = (roof.points[anchorIdx].x - roof.pathOffset.x) / Math.max(baseW, 1e-9)
|
|
const newY = (roof.points[anchorIdx].y - roof.pathOffset.y) / Math.max(baseH, 1e-9)
|
|
roof.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5)
|
|
}
|
|
if (typeof roof.setCoords === 'function') roof.setCoords()
|
|
|
|
// canvas 에 add 된 동일 wallLine 매칭의 eaves(외곽 roofLine) fabric 객체도 같이 갱신.
|
|
// [KERAB-OFFSET-SURGICAL 2026-05-29] outerLine 은 wall 좌표(출폭 0 기준) 유지가 원칙.
|
|
// corner 좌표(=wall + offset*normal) 로 set 하면 출폭 증가 시 외곽 처마라인이 통째로 이동해
|
|
// 화면상 roofLine 이 안 그려진 듯 보임. outerLine 은 attributes 만 갱신, 좌표는 보존.
|
|
const canvasEdgeObjs = canvas
|
|
.getObjects()
|
|
.filter(
|
|
(o) =>
|
|
o.parentId === roof.id &&
|
|
(o.name === 'eaves' || o.lineName === 'roofLine' || o.name === 'outerLine') &&
|
|
o.attributes?.wallLine === target.id,
|
|
)
|
|
for (const eo of canvasEdgeObjs) {
|
|
if (eo.name !== 'outerLine') {
|
|
eo.set({ x1: newCorner1.x, y1: newCorner1.y, x2: newCorner2.x, y2: newCorner2.y, dirty: true })
|
|
}
|
|
eo.attributes = { ...eo.attributes, offset: newOffset, planeSize: newSize, actualSize: newSize }
|
|
if (typeof eo.setCoords === 'function') eo.setCoords()
|
|
}
|
|
if (typeof roof.addLengthText === 'function') roof.addLengthText()
|
|
|
|
canvas.renderAll()
|
|
logger.log(
|
|
'[KERAB-OFFSET-SURGICAL] applied ' +
|
|
JSON.stringify({ idx, oldOffset, newOffset, oldCorner1, newCorner1, oldCorner2, newCorner2 }),
|
|
)
|
|
return true
|
|
}
|