dev #874
@ -77,7 +77,7 @@ export const CalculatorInput = forwardRef(
|
||||
|
||||
newDisplayValue = calculator.currentOperand
|
||||
setDisplayValue(newDisplayValue)
|
||||
onChange(newDisplayValue)
|
||||
onChange?.(newDisplayValue)
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
@ -145,7 +145,7 @@ export const CalculatorInput = forwardRef(
|
||||
newDisplayValue = calculator.currentOperand
|
||||
setDisplayValue(newDisplayValue)
|
||||
if (!hasOperation) {
|
||||
onChange(calculator.currentOperand)
|
||||
onChange?.(calculator.currentOperand)
|
||||
}
|
||||
} else if (num === '.') {
|
||||
if (!calculator.currentOperand.includes('.')) {
|
||||
@ -153,7 +153,7 @@ export const CalculatorInput = forwardRef(
|
||||
newDisplayValue = calculator.currentOperand
|
||||
setDisplayValue(newDisplayValue)
|
||||
if (!hasOperation) {
|
||||
onChange(newDisplayValue)
|
||||
onChange?.(newDisplayValue)
|
||||
}
|
||||
}
|
||||
} else if (!shouldPreventInput(calculator.currentOperand)) {
|
||||
@ -161,7 +161,7 @@ export const CalculatorInput = forwardRef(
|
||||
newDisplayValue = calculator.currentOperand
|
||||
setDisplayValue(newDisplayValue)
|
||||
if (!hasOperation) {
|
||||
onChange(newDisplayValue)
|
||||
onChange?.(newDisplayValue)
|
||||
}
|
||||
}
|
||||
// else {
|
||||
@ -169,7 +169,7 @@ export const CalculatorInput = forwardRef(
|
||||
// newDisplayValue = calculator.currentOperand
|
||||
// setDisplayValue(newDisplayValue)
|
||||
// if (!hasOperation) {
|
||||
// onChange(newDisplayValue)
|
||||
// onChange?.(newDisplayValue)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@ -236,7 +236,7 @@ export const CalculatorInput = forwardRef(
|
||||
const displayValue = newValue || '0'
|
||||
setDisplayValue(displayValue)
|
||||
setHasOperation(false)
|
||||
onChange(displayValue)
|
||||
onChange?.(displayValue)
|
||||
// 커서를 텍스트 끝으로 이동하고 스크롤 처리
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
@ -260,7 +260,7 @@ export const CalculatorInput = forwardRef(
|
||||
setDisplayValue(resultStr)
|
||||
setHasOperation(false)
|
||||
// Only call onChange with the final result
|
||||
onChange(resultStr)
|
||||
onChange?.(resultStr)
|
||||
|
||||
// 엔터키로 호출된 경우 포커스 설정하지 않음
|
||||
if (!fromEnterKey) {
|
||||
@ -282,7 +282,7 @@ export const CalculatorInput = forwardRef(
|
||||
const displayValue = newValue || '0'
|
||||
setDisplayValue(displayValue)
|
||||
setHasOperation(!!calculator.operation)
|
||||
onChange(displayValue)
|
||||
onChange?.(displayValue)
|
||||
// 커서를 텍스트 끝으로 이동하고 스크롤 처리
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
@ -337,10 +337,10 @@ export const CalculatorInput = forwardRef(
|
||||
calculator.currentOperand = filteredValue
|
||||
setHasOperation(false)
|
||||
// 연산자가 없는 순수 숫자일 때만 부모 컴포넌트의 onChange 호출
|
||||
onChange(filteredValue)
|
||||
onChange?.(filteredValue)
|
||||
}
|
||||
|
||||
//onChange(filteredValue)
|
||||
//onChange?.(filteredValue)
|
||||
}
|
||||
}
|
||||
|
||||
@ -485,7 +485,7 @@ export const CalculatorInput = forwardRef(
|
||||
onClick={() => {
|
||||
// const newValue = calculatorRef.current.clear()
|
||||
// setDisplayValue(newValue || '0')
|
||||
// onChange(newValue || '0')
|
||||
// onChange?.(newValue || '0')
|
||||
handleClear()
|
||||
setHasOperation(false)
|
||||
}}
|
||||
@ -497,7 +497,7 @@ export const CalculatorInput = forwardRef(
|
||||
onClick={() => {
|
||||
// const newValue = calculatorRef.current.deleteNumber()
|
||||
// setDisplayValue(newValue || '0')
|
||||
// onChange(newValue || '0')
|
||||
// onChange?.(newValue || '0')
|
||||
//setHasOperation(!!calculatorRef.current.operation)
|
||||
handleDelete()
|
||||
}}
|
||||
@ -532,7 +532,7 @@ export const CalculatorInput = forwardRef(
|
||||
<button
|
||||
onClick={() => {
|
||||
const newValue = calculatorRef.current.appendNumber('.')
|
||||
onChange(newValue)
|
||||
onChange?.(newValue)
|
||||
}}
|
||||
className="btn-number"
|
||||
>
|
||||
|
||||
@ -622,10 +622,13 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) {
|
||||
arrivalPoint = { ...adsorptionPoint }
|
||||
}
|
||||
|
||||
// [2240 MOUSELINE-EVENTED-FALSE 2026-05-20] selectable:false 만으로는 e.target 가로채기 차단 불가 — 클릭이
|
||||
// 십자선에 잡히는 회귀 차단. mouseLine 은 시각 표시만, 좌표 계산은 filter(name==='mouseLine') 로 접근.
|
||||
const horizontalLine = new fabric.Line([-1 * canvas.width, arrivalPoint.y, 2 * canvas.width, arrivalPoint.y], {
|
||||
stroke: 'red',
|
||||
strokeWidth: 1,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
name: 'mouseLine',
|
||||
})
|
||||
|
||||
@ -634,6 +637,7 @@ export function useAuxiliaryDrawing(id, isUseEffect = true) {
|
||||
stroke: 'red',
|
||||
strokeWidth: 1,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
name: 'mouseLine',
|
||||
})
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -385,10 +385,13 @@ export function useEvent() {
|
||||
const lineBottom = visibleBottom + padding
|
||||
|
||||
// 가로선 (수평선)
|
||||
// [2240 MOUSELINE-EVENTED-FALSE 2026-05-20] selectable:false 만으로는 e.target 가로채기 차단 불가 — outerLine 클릭이
|
||||
// 십자선에 잡히는 회귀 차단. mouseLine 은 시각 표시만, 좌표 계산은 filter(name==='mouseLine') 로 접근.
|
||||
const horizontalLine = new fabric.Line([lineLeft, pointer.y, lineRight, pointer.y], {
|
||||
stroke: 'red',
|
||||
strokeWidth: 1,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
direction: 'horizontal',
|
||||
name: 'mouseLine',
|
||||
})
|
||||
@ -398,6 +401,7 @@ export function useEvent() {
|
||||
stroke: 'red',
|
||||
strokeWidth: 1,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
direction: 'vertical',
|
||||
name: 'mouseLine',
|
||||
})
|
||||
|
||||
@ -284,10 +284,13 @@ export function useMode() {
|
||||
}
|
||||
|
||||
// 가로선을 그립니다.
|
||||
// [2240 MOUSELINE-EVENTED-FALSE 2026-05-20] selectable:false 만으로는 e.target 가로채기 차단 불가 — 클릭이
|
||||
// 십자선에 잡히는 회귀 차단. mouseLine 은 시각 표시만, 좌표 계산은 filter(name==='mouseLine') 로 접근.
|
||||
const horizontalLine = new fabric.Line([0, newY, 2 * canvas.width, newY], {
|
||||
stroke: 'red',
|
||||
strokeWidth: 1,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
name: 'mouseLine',
|
||||
})
|
||||
|
||||
@ -296,6 +299,7 @@ export function useMode() {
|
||||
stroke: 'red',
|
||||
strokeWidth: 1,
|
||||
selectable: false,
|
||||
evented: false,
|
||||
name: 'mouseLine',
|
||||
})
|
||||
|
||||
|
||||
157
src/util/kerab-offset-surgical.js
Normal file
157
src/util/kerab-offset-surgical.js
Normal file
@ -0,0 +1,157 @@
|
||||
// [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 { 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 }
|
||||
|
||||
// 옛 corner 좌표에 닿아있던 inner line(hip/ridge/valley/케라바 ext) 끝점을 새 corner 로 snap.
|
||||
const CORNER_SNAP_TOL = 0.5
|
||||
for (const il of roof.innerLines || []) {
|
||||
if (!il) continue
|
||||
if (Math.hypot(il.x1 - oldCorner1.x, il.y1 - oldCorner1.y) <= CORNER_SNAP_TOL) {
|
||||
il.set({ x1: newCorner1.x, y1: newCorner1.y })
|
||||
}
|
||||
if (Math.hypot(il.x2 - oldCorner1.x, il.y2 - oldCorner1.y) <= CORNER_SNAP_TOL) {
|
||||
il.set({ x2: newCorner1.x, y2: newCorner1.y })
|
||||
}
|
||||
if (Math.hypot(il.x1 - oldCorner2.x, il.y1 - oldCorner2.y) <= CORNER_SNAP_TOL) {
|
||||
il.set({ x1: newCorner2.x, y1: newCorner2.y })
|
||||
}
|
||||
if (Math.hypot(il.x2 - oldCorner2.x, il.y2 - oldCorner2.y) <= CORNER_SNAP_TOL) {
|
||||
il.set({ x2: newCorner2.x, y2: newCorner2.y })
|
||||
}
|
||||
}
|
||||
|
||||
// points 는 새 배열로 set 해야 fabric 의 dirty 감지가 동작.
|
||||
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 }
|
||||
})
|
||||
roof.points = newPoints
|
||||
roof.set({ points: newPoints, dirty: true })
|
||||
if (typeof roof.setCoords === 'function') roof.setCoords()
|
||||
|
||||
// canvas 에 add 된 동일 wallLine 매칭의 eaves(외곽 roofLine) fabric 객체도 같이 갱신.
|
||||
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) {
|
||||
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
|
||||
}
|
||||
@ -5155,7 +5155,7 @@ function getTurnDirection(p1, p2, p3) {
|
||||
/**
|
||||
* 현재 점(point)을 기준으로 연결된 이전 라인과 다음 라인을 찾아 골짜기 여부 판단
|
||||
*/
|
||||
function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) {
|
||||
export function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) {
|
||||
const tolerance = 0.1;
|
||||
|
||||
const connectedLineData = {
|
||||
@ -5250,7 +5250,7 @@ function isValleyVertex(targetPoint, connectedLine, allLines, isStartVertex) {
|
||||
return crossProduct > 0;
|
||||
}
|
||||
|
||||
function findInteriorPoint(line, polygonLines) {
|
||||
export function findInteriorPoint(line, polygonLines) {
|
||||
const x1 = line.x1 ?? line.get?.('x1');
|
||||
const y1 = line.y1 ?? line.get?.('y1');
|
||||
const x2 = line.x2 ?? line.get?.('x2');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user