Merge branch 'dev' of https://git.hanasys.jp/qcast3/qcast-front into feature/redo-undo
# Conflicts: # src/hooks/roofcover/useEavesGableEdit.js # src/hooks/roofcover/useRoofAllocationSetting.js
This commit is contained in:
commit
c121937525
@ -48,10 +48,21 @@ function __classifyLineForLabel(obj) {
|
||||
return null
|
||||
}
|
||||
|
||||
export function reattachDebugLabels(canvas, parentId) {
|
||||
__attachDebugLabels(canvas, parentId)
|
||||
}
|
||||
|
||||
function __attachDebugLabels(canvas, parentId) {
|
||||
if (!__isDebugLabelsEnabled()) return
|
||||
if (!canvas || !parentId) return
|
||||
|
||||
// [KERAB-LABEL-REATTACH 2026-05-29] 케라바 토글 등으로 라인이 추가/변경된 후 재호출 가능하도록
|
||||
// 같은 parentId 의 기존 __debugLabel 먼저 제거 → 카운트 reset 후 새로 부여.
|
||||
canvas
|
||||
.getObjects()
|
||||
.filter((o) => o.name === DEBUG_LABEL_NAME && o.parentId === parentId)
|
||||
.forEach((o) => canvas.remove(o))
|
||||
|
||||
const counters = {}
|
||||
const objects = canvas.getObjects().filter((o) => o.parentId === parentId && o.name !== DEBUG_LABEL_NAME)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -30,6 +30,7 @@ import { QcastContext } from '@/app/QcastProvider'
|
||||
import { usePlan } from '@/hooks/usePlan'
|
||||
import { roofsState } from '@/store/roofAtom'
|
||||
import { useText } from '@/hooks/useText'
|
||||
import { fabric } from 'fabric'
|
||||
import { QLine } from '@/components/fabric/QLine'
|
||||
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||
import { calcLineActualSize2 } from '@/util/qpolygon-utils'
|
||||
@ -658,6 +659,247 @@ export function useRoofAllocationSetting(id) {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* [KERAB-VALLEY-OVERLAP-MERGE 2026-05-28] 골짜기 케라바 출폭 띠 — 직사각형 sub-roof 를 인접 sub-roof 들에 union 머지.
|
||||
*
|
||||
* 배경: 케라바 토글 시 두 지붕면이 出幅만큼 서로 물려 겹친다는 표시. useEavesGableEdit.js wallExt 단계에서
|
||||
* 평행 사각형 4 라인(kerabValleyOverlapLine) 으로 출폭 띠를 닫고 split 단계로 진입.
|
||||
* split 후 이 사각형은 별도 sub-roof X 가 됨. 의도는 X 영역이 인접 두 sub-roof 모두에 속해 겹치는 것.
|
||||
* 따라서 X 의 공유 변을 가진 인접 sub-roof 들에 X 의 나머지 변(detour)을 끼워넣어 X 영역을 흡수.
|
||||
* 흡수 완료 후 X 자체는 캔버스에서 제거.
|
||||
*
|
||||
* 식별:
|
||||
* - 직사각형 X: sub.lines 의 attributes.type === 'kerabValleyOverlapLine' 가 N-1 개 이상 (4변 중 3~4)
|
||||
* - 인접 sub-roof: X 의 변과 같은 두 끝점을 공유하는 다른 sub-roof
|
||||
*
|
||||
* 머지: 사각형 X 의 공유 변 1개를 detour 변 N-1개로 대체. 방향은 면적 증가로 검증.
|
||||
*/
|
||||
const mergeValleyOverlapSubRoofs = () => {
|
||||
const newSubRoofs = canvas.getObjects().filter((o) => o.name === POLYGON_TYPE.ROOF && !o.isFixed)
|
||||
if (newSubRoofs.length === 0) return
|
||||
|
||||
const eq = (p, q) => Math.abs(p.x - q.x) < 0.5 && Math.abs(p.y - q.y) < 0.5
|
||||
|
||||
const rects = newSubRoofs.filter((sub) => {
|
||||
if (!sub.lines || sub.lines.length < 3) return false
|
||||
const cnt = sub.lines.filter((l) => l?.attributes?.type === 'kerabValleyOverlapLine').length
|
||||
return cnt >= Math.max(3, sub.lines.length - 1)
|
||||
})
|
||||
|
||||
if (rects.length === 0) return
|
||||
|
||||
logger.log(`[KERAB-VALLEY-OVERLAP-MERGE] 직사각형 sub-roof 후보=${rects.length}`)
|
||||
|
||||
const signedArea = (pts) => {
|
||||
let s = 0
|
||||
for (let i = 0; i < pts.length; i++) {
|
||||
const a = pts[i]
|
||||
const b = pts[(i + 1) % pts.length]
|
||||
s += a.x * b.y - b.x * a.y
|
||||
}
|
||||
return s / 2
|
||||
}
|
||||
|
||||
rects.forEach((X) => {
|
||||
const Xpts = (X.points || []).map((p) => ({ x: p.x, y: p.y }))
|
||||
const Xlines = X.lines || []
|
||||
if (Xpts.length < 3 || Xlines.length < Xpts.length) {
|
||||
logger.log(`[KERAB-VALLEY-OVERLAP-MERGE] X.id=${X.id} skip — pts=${Xpts.length} lines=${Xlines.length}`)
|
||||
return
|
||||
}
|
||||
|
||||
const Xedges = []
|
||||
for (let i = 0; i < Xpts.length; i++) {
|
||||
Xedges.push({ a: Xpts[i], b: Xpts[(i + 1) % Xpts.length], idx: i })
|
||||
}
|
||||
|
||||
// 인접 sub-roof — X 와 공유 변 모두 수집 (다중 공유 검출용)
|
||||
const adjacencyMap = new Map() // sub → [{ subEdgeStart, xEdgeIdx }, ...]
|
||||
newSubRoofs.forEach((sub) => {
|
||||
if (sub === X) return
|
||||
const pts = sub.points || []
|
||||
if (pts.length < 3) return
|
||||
const shares = []
|
||||
for (let i = 0; i < pts.length; i++) {
|
||||
const p1 = pts[i]
|
||||
const p2 = pts[(i + 1) % pts.length]
|
||||
for (const e of Xedges) {
|
||||
if ((eq(p1, e.a) && eq(p2, e.b)) || (eq(p1, e.b) && eq(p2, e.a))) {
|
||||
shares.push({ subEdgeStart: i, xEdgeIdx: e.idx })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shares.length > 0) adjacencyMap.set(sub, shares)
|
||||
})
|
||||
|
||||
logger.log(
|
||||
`[KERAB-VALLEY-OVERLAP-MERGE] X.id=${X.id} pts=${Xpts.length} adjacents=${adjacencyMap.size} ` +
|
||||
`shares=[${[...adjacencyMap.values()].map((s) => s.length).join(',')}]`,
|
||||
)
|
||||
|
||||
if (adjacencyMap.size === 0) {
|
||||
canvas.remove(X)
|
||||
return
|
||||
}
|
||||
|
||||
adjacencyMap.forEach((shares, sub) => {
|
||||
// 다중 공유 → bowtie 위험, 머지 skip
|
||||
if (shares.length !== 1) {
|
||||
logger.log(`[KERAB-VALLEY-OVERLAP-MERGE] sub.id=${sub.id} skip — sharedEdges=${shares.length} (≠1, bowtie 위험)`)
|
||||
return
|
||||
}
|
||||
const { subEdgeStart, xEdgeIdx } = shares[0]
|
||||
const pts = (sub.points || []).map((p) => ({ x: p.x, y: p.y }))
|
||||
const oldLines = [...(sub.lines || [])]
|
||||
const N = Xpts.length
|
||||
|
||||
// [KERAB-VALLEY-OVERLAP-MERGE 2026-05-28] detour 방향은 sub.pts[subEdgeStart] 가
|
||||
// X.xEdge.a (=Xpts[xEdgeIdx]) 인지 X.xEdge.b (=Xpts[xEdgeIdx+1]) 인지로 결정.
|
||||
// sub 외곽선 진행 방향이 xEdge 의 a→b 와 같으면 detour 는 X 의 a 쪽 이웃부터 (역방향)
|
||||
// 반대면 detour 는 X 의 b 쪽 이웃부터 (정방향).
|
||||
const subA = pts[subEdgeStart]
|
||||
const xA = Xpts[xEdgeIdx]
|
||||
const sameDir = eq(subA, xA)
|
||||
|
||||
const detour = []
|
||||
const detourEdgeIdx = []
|
||||
if (sameDir) {
|
||||
// a → Xpts[i-1] → Xpts[i-2] → ... → Xpts[i+2] → b
|
||||
// detour vertices: N-2 개 (a, b 사이)
|
||||
for (let k = 1; k <= N - 2; k++) {
|
||||
detour.push(Xpts[(xEdgeIdx - k + N) % N])
|
||||
}
|
||||
// detour edges: N-1 개. a→Xpts[i-1] 는 X 의 (i-1)%N 변 (반대방향).
|
||||
for (let k = 1; k <= N - 1; k++) {
|
||||
detourEdgeIdx.push((xEdgeIdx - k + N) % N)
|
||||
}
|
||||
} else {
|
||||
// b → Xpts[i+2] → Xpts[i+3] → ... → Xpts[i+N-1] → a 의 a→b 표기:
|
||||
// a → Xpts[i+2] → Xpts[i+3] → ... → Xpts[i+N-1] → b (subA=b 인 관점에서 보면 반대)
|
||||
// 정확히는 subA=X.xEdge.b → detour 시작 = Xpts[(i+2)%N] (b 의 X 내 이웃)
|
||||
for (let k = 2; k <= N - 1; k++) {
|
||||
detour.push(Xpts[(xEdgeIdx + k) % N])
|
||||
}
|
||||
for (let k = 1; k <= N - 1; k++) {
|
||||
detourEdgeIdx.push((xEdgeIdx + k) % N)
|
||||
}
|
||||
}
|
||||
|
||||
const buildCand = (verts) => {
|
||||
const c = [...pts]
|
||||
c.splice(subEdgeStart + 1, 0, ...verts)
|
||||
return c
|
||||
}
|
||||
|
||||
const candA = buildCand(detour)
|
||||
const oldSigned = signedArea(pts)
|
||||
const aSigned = signedArea(candA)
|
||||
const sameSign = (s) => oldSigned === 0 || Math.sign(s) === Math.sign(oldSigned)
|
||||
const okA = sameSign(aSigned) && Math.abs(aSigned) > Math.abs(oldSigned) + 0.5
|
||||
|
||||
let finalPts = null
|
||||
let finalEdges = null
|
||||
if (okA) {
|
||||
finalPts = candA
|
||||
finalEdges = detourEdgeIdx
|
||||
} else {
|
||||
// fallback — 반대 방향 시도 (X 가 CW 로 들어온 경우 대비)
|
||||
const detourRev = [...detour].reverse()
|
||||
const detourEdgeRev = [...detourEdgeIdx].reverse()
|
||||
const candB = buildCand(detourRev)
|
||||
const bSigned = signedArea(candB)
|
||||
const okB = sameSign(bSigned) && Math.abs(bSigned) > Math.abs(oldSigned) + 0.5
|
||||
if (okB) {
|
||||
finalPts = candB
|
||||
finalEdges = detourEdgeRev
|
||||
logger.log(
|
||||
`[KERAB-VALLEY-OVERLAP-MERGE] sub.id=${sub.id} fallback reverse detour 사용 ` +
|
||||
`oldSigned=${oldSigned.toFixed(0)} → ${bSigned.toFixed(0)}`,
|
||||
)
|
||||
} else {
|
||||
logger.log(
|
||||
`[KERAB-VALLEY-OVERLAP-MERGE] sub.id=${sub.id} skip — detour 양방향 실패 ` +
|
||||
`sameDir=${sameDir} oldSigned=${oldSigned.toFixed(0)} ` +
|
||||
`aSigned=${aSigned.toFixed(0)} bSigned=${bSigned.toFixed(0)} ` +
|
||||
`subA=(${subA.x.toFixed(1)},${subA.y.toFixed(1)}) ` +
|
||||
`xA=(${xA.x.toFixed(1)},${xA.y.toFixed(1)}) ` +
|
||||
`xB=(${Xpts[(xEdgeIdx + 1) % N].x.toFixed(1)},${Xpts[(xEdgeIdx + 1) % N].y.toFixed(1)})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// sub.lines 재구성 — 옛 공유 변 1개 → detour 변 N-1개
|
||||
const newSubLines = []
|
||||
for (let i = 0; i < oldLines.length; i++) {
|
||||
if (i === subEdgeStart) {
|
||||
for (let k = 0; k < finalEdges.length; k++) {
|
||||
const xLine = Xlines[finalEdges[k]]
|
||||
const p1 = finalPts[(subEdgeStart + k) % finalPts.length]
|
||||
const p2 = finalPts[(subEdgeStart + k + 1) % finalPts.length]
|
||||
const attrs = xLine?.attributes ? { ...xLine.attributes } : { type: 'kerabValleyOverlapLine', offset: 0 }
|
||||
const ln = new QLine([p1.x, p1.y, p2.x, p2.y], {
|
||||
stroke: sub.stroke,
|
||||
strokeWidth: sub.strokeWidth,
|
||||
fontSize: sub.fontSize,
|
||||
attributes: attrs,
|
||||
textVisible: false,
|
||||
parent: sub,
|
||||
parentId: sub.id,
|
||||
idx: newSubLines.length + 1,
|
||||
})
|
||||
ln.startPoint = p1
|
||||
ln.endPoint = p2
|
||||
newSubLines.push(ln)
|
||||
}
|
||||
} else {
|
||||
const ln = oldLines[i]
|
||||
if (ln) {
|
||||
ln.idx = newSubLines.length + 1
|
||||
newSubLines.push(ln)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [KERAB-VALLEY-OVERLAP-MERGE 2026-05-28] fabric.Polygon 정식 points 갱신 패턴.
|
||||
// src/util/canvas-util.js#anchorWrapper 패턴 그대로 적용 —
|
||||
// _setPositionDimensions 가 width/height/pathOffset/left/top 을 재계산하므로
|
||||
// 변경 전 앵커(pts[0]) 절대좌표를 캡쳐 → 변경 후 setPositionByOrigin 으로 복원.
|
||||
// 그래야 polygon 의 path 가 새 bbox 기준으로 다시 그려지면서 시각 위치도 유지됨.
|
||||
const anchorIdx = 0
|
||||
const oldLocal = {
|
||||
x: sub.points[anchorIdx].x - sub.pathOffset.x,
|
||||
y: sub.points[anchorIdx].y - sub.pathOffset.y,
|
||||
}
|
||||
const absolutePoint = fabric.util.transformPoint(oldLocal, sub.calcTransformMatrix())
|
||||
|
||||
sub.points = finalPts
|
||||
sub.lines = newSubLines
|
||||
sub._setPositionDimensions({})
|
||||
|
||||
const strokeW = sub.strokeUniform ? sub.strokeWidth / sub.scaleX : sub.strokeWidth
|
||||
const baseW = sub.width + strokeW
|
||||
const baseH = sub.height + (sub.strokeUniform ? sub.strokeWidth / sub.scaleY : sub.strokeWidth)
|
||||
const newX = (sub.points[anchorIdx].x - sub.pathOffset.x) / Math.max(baseW, 1e-9)
|
||||
const newY = (sub.points[anchorIdx].y - sub.pathOffset.y) / Math.max(baseH, 1e-9)
|
||||
sub.setPositionByOrigin(absolutePoint, newX + 0.5, newY + 0.5)
|
||||
sub.setCoords?.()
|
||||
sub.dirty = true
|
||||
|
||||
logger.log(
|
||||
`[KERAB-VALLEY-OVERLAP-MERGE] sub.id=${sub.id} 머지 완료 ` +
|
||||
`oldSigned=${oldSigned.toFixed(0)} → newSigned=${signedArea(finalPts).toFixed(0)} ` +
|
||||
`pts=${finalPts.length} lines=${newSubLines.length}`,
|
||||
)
|
||||
})
|
||||
|
||||
canvas.remove(X)
|
||||
})
|
||||
|
||||
canvas.renderAll?.()
|
||||
}
|
||||
|
||||
/**
|
||||
* 지붕면 할당
|
||||
*/
|
||||
@ -669,9 +911,16 @@ export function useRoofAllocationSetting(id) {
|
||||
roofBases.forEach((roofBase) => {
|
||||
try {
|
||||
// 지붕 할당 로직에 extensionLine 추가
|
||||
|
||||
// [KERAB-VALLEY-OVERLAP 2026-05-28] 골짜기 케라바 출폭 띠 외곽 라인(kerabValleyOverlapLine) 도 할당 대상.
|
||||
// b polygon 의 lines[] 에 추가되어 b 의 면이 출폭 띠 영역까지 확장 → a 의 sub-면과 겹침 표현.
|
||||
const roofEaveHelpLines = canvas
|
||||
.getObjects()
|
||||
.filter((obj) => (obj.lineName === 'eaveHelpLine' || obj.lineName === 'extensionLine') && obj.roofId === roofBase.id)
|
||||
.filter(
|
||||
(obj) =>
|
||||
(obj.lineName === 'eaveHelpLine' || obj.lineName === 'extensionLine' || obj.lineName === 'kerabValleyOverlapLine') &&
|
||||
obj.roofId === roofBase.id,
|
||||
)
|
||||
// logger.log('roofBase.id:', roofBase.id)
|
||||
// logger.log('roofEaveHelpLines:', roofEaveHelpLines)
|
||||
// logger.log('extensionLines found:', roofEaveHelpLines.filter(l => l.lineName === 'extensionLine'))
|
||||
@ -686,6 +935,8 @@ export function useRoofAllocationSetting(id) {
|
||||
|
||||
const extensionLines = newEaveLines.filter((line) => line.lineName === 'extensionLine')
|
||||
const normalEaveLines = newEaveLines.filter((line) => line.lineName === 'eaveHelpLine')
|
||||
// [KERAB-VALLEY-OVERLAP 2026-05-28] 골짜기 케라바 겹침 라인 — overlap 판단 제외, 단순 결합
|
||||
const overlapLines = newEaveLines.filter((line) => line.lineName === 'kerabValleyOverlapLine')
|
||||
// logger.log('extensionLines count:', extensionLines.length)
|
||||
// logger.log('normalEaveLines count:', normalEaveLines.length)
|
||||
|
||||
@ -743,12 +994,13 @@ export function useRoofAllocationSetting(id) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false // 끝점만 닿아 있거나 직각인 경우는 살림
|
||||
})
|
||||
return !shouldRemove
|
||||
})
|
||||
// Combine remaining lines with newEaveLines
|
||||
roofBase.lines = [...linesToKeep, ...normalEaveLines, ...extensionLines]
|
||||
roofBase.lines = [...linesToKeep, ...normalEaveLines, ...extensionLines, ...overlapLines]
|
||||
} else {
|
||||
roofBase.lines = [...roofEaveHelpLines]
|
||||
}
|
||||
@ -816,6 +1068,12 @@ export function useRoofAllocationSetting(id) {
|
||||
const __newRoofs = canvas.getObjects().filter((o) => o.name === 'roof' && !o.isFixed)
|
||||
logger.log(`[KERAB-PATTERN-DIAG] 분할 후 새 sub-roof=${__newRoofs.length}`)
|
||||
}
|
||||
|
||||
// [KERAB-VALLEY-OVERLAP-MERGE 2026-05-28] 직사각형 sub-roof 를 인접 두 sub-roof 에 union 머지.
|
||||
// 골짜기 출폭 띠 영역이 두 면 모두에 속하도록 (겹침 표현).
|
||||
// 직사각형 식별: 외곽 라인의 attributes.type === 'kerabValleyOverlapLine' 가 다수인 sub-roof.
|
||||
// 인접 두 sub-roof = 직사각형의 변 좌표를 공유하는 다른 sub-roof.
|
||||
mergeValleyOverlapSubRoofs()
|
||||
} catch (e) {
|
||||
logger.log(e)
|
||||
canvas.discardActiveObject()
|
||||
@ -827,6 +1085,18 @@ export function useRoofAllocationSetting(id) {
|
||||
canvas.remove(line)
|
||||
})
|
||||
|
||||
// [KERAB-VALLEY-OVERLAP 2026-05-28] roofBase.lines 로 추가된 보조 라인(kerabValleyOverlapLine) 및
|
||||
// wallBase 변형 kerabPatternValleyExt(innerLines 미포함) 는 split 이후 더 이상 필요 없으므로 정리.
|
||||
// 미정리 시 canvas 잔류 → sub-roof 위에 솔리드/점선으로 남는다.
|
||||
const overlapLeftovers = canvas
|
||||
.getObjects()
|
||||
.filter(
|
||||
(obj) =>
|
||||
(obj.lineName === 'kerabValleyOverlapLine' || obj.lineName === 'kerabPatternValleyExt') &&
|
||||
(obj.roofId === roofBase.id || obj?.attributes?.roofId === roofBase.id),
|
||||
)
|
||||
overlapLeftovers.forEach((line) => canvas.remove(line))
|
||||
|
||||
canvas.remove(roofBase)
|
||||
})
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
// 고객 회귀 검증을 위해 본 로직 전체를 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'
|
||||
@ -123,16 +124,52 @@ export const applyKerabOffsetSurgical = (canvas, target, newOffset) => {
|
||||
}
|
||||
|
||||
// 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(
|
||||
@ -142,7 +179,9 @@ export const applyKerabOffsetSurgical = (canvas, target, newOffset) => {
|
||||
o.attributes?.wallLine === target.id,
|
||||
)
|
||||
for (const eo of canvasEdgeObjs) {
|
||||
eo.set({ x1: newCorner1.x, y1: newCorner1.y, x2: newCorner2.x, y2: newCorner2.y, dirty: true })
|
||||
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()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user