dev #862
@ -93,7 +93,12 @@ export const QLine = fabric.util.createClass(fabric.Line, {
|
||||
thisText.set({ actualSize: this.attributes.actualSize })
|
||||
}
|
||||
if (this.attributes?.planeSize) {
|
||||
thisText.set({ planeSize: this.attributes.planeSize, text: this.attributes.planeSize.toString() })
|
||||
// [2026-05-18 라벨 저장값 통일] planeSize 저장값 그대로 표시.
|
||||
// 정수(calcLinePlaneSize) 또는 0.5 step(hip equalize 후) 모두 노출.
|
||||
thisText.set({
|
||||
planeSize: this.attributes.planeSize,
|
||||
text: Number(this.attributes.planeSize).toFixed(1).replace(/\.0$/, ''),
|
||||
})
|
||||
}
|
||||
// 라인이 이동했을 수 있으므로 위치도 재계산해서 업데이트한다.
|
||||
const sX = this.scaleX
|
||||
@ -147,8 +152,9 @@ export const QLine = fabric.util.createClass(fabric.Line, {
|
||||
const maxY = this.top + this.length
|
||||
const degree = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI
|
||||
|
||||
// [2026-05-18 라벨 저장값 통일] planeSize 저장값(정수 또는 0.5 step) 그대로.
|
||||
const displayValue = this.attributes?.planeSize ?? this.getLength()
|
||||
const text = new fabric.Textbox(displayValue.toString(), {
|
||||
const text = new fabric.Textbox(Number(displayValue).toFixed(1).replace(/\.0$/, ''), {
|
||||
actualSize: this.attributes?.actualSize,
|
||||
planeSize: this.attributes?.planeSize,
|
||||
left: left,
|
||||
|
||||
@ -2,7 +2,7 @@ import { fabric } from 'fabric'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { QLine } from '@/components/fabric/QLine'
|
||||
import { distanceBetweenPoints, findTopTwoIndexesByDistance, getDirectionByPoint, sortedPointLessEightPoint, sortedPoints } from '@/util/canvas-util'
|
||||
import { calculateAngle, drawGableRoof, drawRoofByAttribute, drawShedRoof, snapNearAxisEdges, toGeoJSON } from '@/util/qpolygon-utils'
|
||||
import { calculateAngle, drawGableRoof, drawRoofByAttribute, drawShedRoof, equalizeSymmetricHips, snapNearAxisEdges, toGeoJSON } from '@/util/qpolygon-utils'
|
||||
import * as turf from '@turf/turf'
|
||||
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
|
||||
import Big from 'big.js'
|
||||
@ -610,49 +610,11 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
drawRoofByAttribute(this.id, this.canvas, textMode)
|
||||
}
|
||||
|
||||
// 대칭 hip line 쌍의 planeSize/actualSize 를 평균값으로 통일 (예: 1637 / 1638 → 1637.5)
|
||||
// hip 라인은 좌우 대칭이지만 부동소수점 round-off 로 1mm 차이가 발생함
|
||||
// 대칭 hip line 쌍의 planeSize/actualSize 통일 (±5mm 그룹 평균, 0.5 step)
|
||||
// 부동소수점 round-off 로 좌우 hip 이 1mm 차이 나는 케이스 보정.
|
||||
if (this.innerLines && this.innerLines.length > 0) {
|
||||
const hips = this.innerLines.filter((l) => l && l.name === LINE_TYPE.SUBLINE.HIP)
|
||||
const _segLen = (l) => Math.round(Math.sqrt((l.x2 - l.x1) ** 2 + (l.y2 - l.y1) ** 2) * 10)
|
||||
const _equalizeText = (line, plane, actual) => {
|
||||
if (line.attributes) {
|
||||
line.attributes.planeSize = plane
|
||||
line.attributes.actualSize = actual
|
||||
}
|
||||
// canvas 위 lengthText 도 즉시 갱신
|
||||
const text = this.canvas.getObjects().find((o) => o.name === 'lengthText' && o.parentId === line.id)
|
||||
if (text) {
|
||||
text.set({ planeSize: plane, actualSize: actual })
|
||||
// 현재 표시 모드에 맞춰 text 갱신
|
||||
const isPlane = !text.actualSize || text.text === String(text.planeSize)
|
||||
text.set({ text: isPlane ? String(plane) : String(actual) })
|
||||
}
|
||||
}
|
||||
// 길이 그룹화: ±5mm 이내 길이끼리 묶고, 그 그룹 안에서 평균값으로 통일
|
||||
const used = new Set()
|
||||
hips.forEach((h, i) => {
|
||||
if (used.has(i)) return
|
||||
const group = [{ idx: i, line: h }]
|
||||
const baseLen = _segLen(h)
|
||||
hips.forEach((h2, j) => {
|
||||
if (j <= i || used.has(j)) return
|
||||
if (Math.abs(_segLen(h2) - baseLen) <= 5) group.push({ idx: j, line: h2 })
|
||||
})
|
||||
if (group.length >= 2) {
|
||||
const avgPlane = group.reduce((s, g) => s + (g.line.attributes?.planeSize || 0), 0) / group.length
|
||||
const avgActual = group.reduce((s, g) => s + (g.line.attributes?.actualSize || 0), 0) / group.length
|
||||
// 0.5 단위 정밀도 유지
|
||||
const plane = Math.round(avgPlane * 2) / 2
|
||||
const actual = Math.round(avgActual * 2) / 2
|
||||
group.forEach(({ idx, line }) => {
|
||||
used.add(idx)
|
||||
_equalizeText(line, plane, actual)
|
||||
})
|
||||
} else {
|
||||
used.add(i)
|
||||
}
|
||||
})
|
||||
equalizeSymmetricHips(hips, this.canvas)
|
||||
this.canvas?.renderAll()
|
||||
}
|
||||
|
||||
@ -697,6 +659,9 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
const dx = Big(end.x).minus(Big(start.x))
|
||||
const dy = Big(end.y).minus(Big(start.y))
|
||||
const length = dx.pow(2).plus(dy.pow(2)).sqrt().times(10).round().toNumber()
|
||||
// [2026-05-18 라벨 저장값 통일] planeSize(정수 또는 0.5 step) 그대로.
|
||||
const __displayRaw = this.lines[i]?.attributes?.planeSize ?? length
|
||||
const __display = Number(__displayRaw).toFixed(1).replace(/\.0$/, '')
|
||||
|
||||
const direction = getDirectionByPoint(start, end)
|
||||
|
||||
@ -723,7 +688,7 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
const degree = Big(Math.atan2(dy.toNumber(), dx.toNumber())).times(180).div(Math.PI).toNumber()
|
||||
|
||||
// Create a new text object if it doesn't exist
|
||||
const text = new fabric.Text(length.toString(), {
|
||||
const text = new fabric.Text(__display, {
|
||||
left: midPoint.x,
|
||||
top: midPoint.y,
|
||||
fontSize: this.fontSize,
|
||||
|
||||
@ -17,7 +17,8 @@ import { useMouse } from '@/hooks/useMouse'
|
||||
// 'passthrough' : 가드 전면 스킵 — 사용자가 입력한 raw 값을 그대로 적용. (과거 crossed 허용 동작; polygon 왜곡 위험)
|
||||
// 교체 방법: 이 상수만 변경. OVER_GUARD 분기가 자동 반응.
|
||||
const OVER_MOVE_POLICY = 'clamp'
|
||||
const OVER_EPS = 0.5
|
||||
// [2026-05-18 라벨 5mm 단축 fix 1차] 0.5 → 0.1. 인접 edge 잔여 1mm → SHOULDER_ABSORBED(<10) 자연 흡수, 라벨 1mm 차는 round-trip 인정 범위.
|
||||
const OVER_EPS = 0.1
|
||||
|
||||
//동선이동 형 올림 내림
|
||||
export function useMovementSetting(id) {
|
||||
|
||||
@ -147,7 +147,9 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
|
||||
}
|
||||
}
|
||||
|
||||
const displayValue = (+roofSizeSet === 1 ? actualSize : planeSize).toString()
|
||||
// [2026-05-18 라벨 저장값 통일] planeSize/actualSize 저장값 그대로.
|
||||
const __raw = +roofSizeSet === 1 ? actualSize : planeSize
|
||||
const displayValue = Number(__raw).toFixed(1).replace(/\.0$/, '')
|
||||
let tx = (start.x + end.x) / 2
|
||||
let ty = (start.y + end.y) / 2
|
||||
if (lineDir === 'right') ty += 15
|
||||
|
||||
@ -3,7 +3,7 @@ import { useRecoilValue } from 'recoil'
|
||||
import { fabric } from 'fabric'
|
||||
import { calculateIntersection, findAndRemoveClosestPoint, getDegreeByChon, isPointOnLine } from '@/util/canvas-util'
|
||||
import { QPolygon } from '@/components/fabric/QPolygon'
|
||||
import { calcLinePlaneSize, isSamePoint, removeDuplicatePolygons } from '@/util/qpolygon-utils'
|
||||
import { calcLinePlaneSize, equalizeSymmetricHips, isSamePoint, removeDuplicatePolygons } from '@/util/qpolygon-utils'
|
||||
import { basicSettingState, flowDisplaySelector } from '@/store/settingAtom'
|
||||
import { fontSelector } from '@/store/fontAtom'
|
||||
import { QLine } from '@/components/fabric/QLine'
|
||||
@ -89,8 +89,10 @@ export const usePolygon = () => {
|
||||
const maxY = line.top + line.length
|
||||
const degree = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI
|
||||
|
||||
// [2026-05-18 라벨 저장값 통일] planeSize(정수 또는 0.5 step) 그대로.
|
||||
const __display = planeSize ?? length
|
||||
const text = new fabric.Textbox(
|
||||
+roofSizeSet === 1 ? (actualSize ? actualSize.toString() : length.toString()) : planeSize ? planeSize.toString() : length.toString(),
|
||||
Number(__display).toFixed(1).replace(/\.0$/, ''),
|
||||
{
|
||||
left: left,
|
||||
top: top,
|
||||
@ -2139,6 +2141,26 @@ export const usePolygon = () => {
|
||||
setActualSize(line, polygon.direction, +polygon.roofMaterial?.pitch, forceUpdate)
|
||||
})
|
||||
|
||||
// [hip 대칭 통일 2026-05-18] 할당 단계도 SK 빌드와 동일하게 hip 좌우대칭 라인을
|
||||
// 평균 0.5 step 으로 통일. 좌표 float drift 로 hip 4개가 3832.5/3831.5/3833.5
|
||||
// 처럼 갈리던 문제 차단.
|
||||
// canvas 전체 ROOF 폴리곤의 hip 라인을 모아 cross-polygon 그룹화 (한 hip 이 두 face 에
|
||||
// 공유되는 경우도 같은 그룹).
|
||||
const allHips = []
|
||||
const seenIds = new Set()
|
||||
canvas?.getObjects()?.forEach((obj) => {
|
||||
if (obj?.name !== POLYGON_TYPE.ROOF || !obj.lines) return
|
||||
obj.lines.forEach((l) => {
|
||||
if (l?.attributes?.type === LINE_TYPE.SUBLINE.HIP && !seenIds.has(l.id)) {
|
||||
seenIds.add(l.id)
|
||||
allHips.push(l)
|
||||
}
|
||||
})
|
||||
})
|
||||
if (allHips.length >= 2) {
|
||||
equalizeSymmetricHips(allHips, canvas)
|
||||
}
|
||||
|
||||
addLengthText(polygon)
|
||||
}
|
||||
|
||||
|
||||
@ -96,12 +96,15 @@ export function useText() {
|
||||
return newActual
|
||||
}
|
||||
|
||||
// [2026-05-18 라벨 통일] planeSize/actualSize 모두 저장값 그대로 표시.
|
||||
// planeSize 는 정수(calcLinePlaneSize) 가 기본, hip equalize 후 0.5 step 가능.
|
||||
// actualSize 는 0.5 step. 둘 다 .toFixed(1) 후 trailing .0 제거.
|
||||
switch (column) {
|
||||
case 'corridorDimension':
|
||||
lengthTexts.forEach((obj) => {
|
||||
const v = resolveSize(obj, 'planeSize')
|
||||
if (v) {
|
||||
obj.set({ text: v.toString() })
|
||||
obj.set({ text: Number(v).toFixed(1).replace(/\.0$/, '') })
|
||||
}
|
||||
})
|
||||
break
|
||||
@ -111,7 +114,7 @@ export function useText() {
|
||||
recomputeHipActualIfFlat(obj)
|
||||
const v = resolveSize(obj, 'actualSize')
|
||||
if (v) {
|
||||
obj.set({ text: v.toString() })
|
||||
obj.set({ text: Number(v).toFixed(1).replace(/\.0$/, '') })
|
||||
}
|
||||
})
|
||||
break
|
||||
@ -207,7 +210,9 @@ export function useText() {
|
||||
for (const group of groups) {
|
||||
if (group.items.length < 2) continue
|
||||
const avg = group.items.reduce((s, i) => s + i.value, 0) / group.items.length
|
||||
const unified = Math.round(avg).toString()
|
||||
// [2026-05-18 라벨 0.5 step 통일] corridor/real 모두 0.5 step.
|
||||
// planeSize 도 hip equalize 후 0.5 step 가능하므로 정수 round 하지 않음.
|
||||
const unified = (Math.round(avg * 2) / 2).toFixed(1).replace(/\.0$/, '')
|
||||
for (const item of group.items) {
|
||||
// text 만 set. obj.actualSize / obj.planeSize 는 그대로.
|
||||
item.obj.set({ text: unified })
|
||||
|
||||
@ -6276,14 +6276,74 @@ export const calcLineActualSize = (points, degree = 0) => {
|
||||
export const calcLineActualSize2 = (points, degree = 0) => {
|
||||
const planeSize = calcLinePlaneSize(points)
|
||||
|
||||
// [공식 통일 2026-05-18] setActualSize(useLine.js) 와 동일하게 axis 평균 사용.
|
||||
// 기존: hx = xLength * tan(deg) → X 축만 → south/north hip 과 west/east hip 에서
|
||||
// symmetric 케이스에도 다른 actualSize 가 산출되어 SK 빌드와 할당 단계 라벨 불일치.
|
||||
// 변경: axisLength = (xLength + yLength) / 2 → 양 face 대칭값 산출.
|
||||
const xLength = Math.abs(points.x2 - points.x1) * 10
|
||||
const yLength = Math.abs(points.y2 - points.y1) * 10
|
||||
const hx = xLength * Math.tan(degree * (Math.PI / 180))
|
||||
const axisLength = (xLength + yLength) / 2
|
||||
const h = axisLength * Math.tan(degree * (Math.PI / 180))
|
||||
const actualSize = Math.sqrt(h ** 2 + planeSize ** 2)
|
||||
// setActualSize 와 동일하게 0.5 step 으로 정합.
|
||||
return Number((Math.round(actualSize * 2) / 2).toFixed(1))
|
||||
}
|
||||
|
||||
const hy = yLength * Math.tan(degree * (Math.PI / 180))
|
||||
const actualSize = Number(Math.sqrt(hx ** 2 + planeSize ** 2)).toFixed(0)
|
||||
const actualSize2 = Number(Math.sqrt(hy ** 2 + planeSize ** 2)).toFixed(0)
|
||||
return actualSize
|
||||
/**
|
||||
* 대칭 hip 라인들의 planeSize/actualSize 를 ±5mm 그룹 평균(0.5 step)으로 통일.
|
||||
* SK 빌드(QPolygon.drawHelpLine innerLines) 와 지붕재 할당(setPolygonLinesActualSize 결과) 양쪽에서 재사용.
|
||||
*
|
||||
* @param {Array} hipLines - hip 라인 객체 배열 (x1,y1,x2,y2 + attributes 필수)
|
||||
* @param {object} canvas - fabric canvas (lengthText 동기화용; null 이면 attribute 만 갱신)
|
||||
*/
|
||||
export const equalizeSymmetricHips = (hipLines, canvas) => {
|
||||
if (!hipLines || hipLines.length < 2) return
|
||||
// [그룹 키 안정화 2026-05-18]
|
||||
// 기존 rawLen(Math.hypot * 10) 기준은 같은 hip 의 face별 좌표 미세 drift
|
||||
// (예: dx=290,dy=289 vs 290,290 → 7mm 차) 로 ±5mm 그룹을 벗어남.
|
||||
// planeSize 는 SK 빌드시 이미 ±5mm 그룹으로 통일된 안정 정수 키.
|
||||
// planeSize 없을 때만 rawLen 폴백.
|
||||
const _segLen = (l) =>
|
||||
l?.attributes?.planeSize != null
|
||||
? Math.round(l.attributes.planeSize)
|
||||
: Math.round(Math.sqrt((l.x2 - l.x1) ** 2 + (l.y2 - l.y1) ** 2) * 10)
|
||||
const _apply = (line, plane, actual) => {
|
||||
if (line.attributes) {
|
||||
line.attributes.planeSize = plane
|
||||
line.attributes.actualSize = actual
|
||||
}
|
||||
if (!canvas) return
|
||||
const text = canvas.getObjects().find((o) => o.name === 'lengthText' && o.parentId === line.id)
|
||||
if (text) {
|
||||
text.set({ planeSize: plane, actualSize: actual })
|
||||
const isPlane = !text.actualSize || text.text === String(text.planeSize)
|
||||
// planeSize/actualSize 모두 저장값(정수 또는 0.5 step) 그대로 표시.
|
||||
const __raw = isPlane ? plane : actual
|
||||
text.set({ text: Number(__raw).toFixed(1).replace(/\.0$/, '') })
|
||||
}
|
||||
}
|
||||
const used = new Set()
|
||||
hipLines.forEach((h, i) => {
|
||||
if (used.has(i)) return
|
||||
const group = [{ idx: i, line: h }]
|
||||
const baseLen = _segLen(h)
|
||||
hipLines.forEach((h2, j) => {
|
||||
if (j <= i || used.has(j)) return
|
||||
if (Math.abs(_segLen(h2) - baseLen) <= 5) group.push({ idx: j, line: h2 })
|
||||
})
|
||||
if (group.length >= 2) {
|
||||
const avgPlane = group.reduce((s, g) => s + (g.line.attributes?.planeSize || 0), 0) / group.length
|
||||
const avgActual = group.reduce((s, g) => s + (g.line.attributes?.actualSize || 0), 0) / group.length
|
||||
const plane = Math.round(avgPlane * 2) / 2
|
||||
const actual = Math.round(avgActual * 2) / 2
|
||||
group.forEach(({ idx, line }) => {
|
||||
used.add(idx)
|
||||
_apply(line, plane, actual)
|
||||
})
|
||||
} else {
|
||||
used.add(i)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -2918,12 +2918,28 @@ function processEavesEdge(roofId, canvas, skeleton, edgeResult, skeletonLines) {
|
||||
// logger.log('Has matching line:', outerLine);
|
||||
//if(outerLine === null) return
|
||||
}
|
||||
// [hip pitch fallback 2026-04-30]
|
||||
// outerLine.attributes.pitch 가 없으면 0 → calcLineActualSize2(deg=0) 가
|
||||
// calcLinePlaneSize 와 동치(tan0=0) → 모든 hip 의 planeSize === actualSize.
|
||||
// 결과: 伏せ図(plane) ↔ 配置面(actual) 메뉴 토글해도 hip 라벨 변화 없음.
|
||||
// roof.pitch 폴백(usePolygon.js:1649 패턴) 으로 진짜 pitch 확보.
|
||||
let pitch = outerLine?.attributes?.pitch ?? roof?.pitch ?? 0
|
||||
// [hip pitch fallback 2026-04-30 / 강화 2026-05-18]
|
||||
// 1) outerLine.attributes.pitch — 매칭된 eaves 라인의 pitch
|
||||
// 2) roof.pitch — setRoofPitch 에서 stamp (allocation 후)
|
||||
// 3) roof.lines 중 pitch 가 있는 eaves 라인 — 초기 SK 빌드 시 매칭 실패 시
|
||||
// 4) canvas 의 wall outerLine 중 pitch 가 있는 라인 — 최후 폴백
|
||||
// 5) 0
|
||||
// 기존엔 1~2 폴백만 있어 SK 빌드시 pitch=0 → hip actualSize=planeSize 로 잘못 계산되고,
|
||||
// 이후 display recompute / 할당 단계에서 다른 pitch 로 재계산되어 라벨이 변하는 문제.
|
||||
let pitch = outerLine?.attributes?.pitch ?? roof?.pitch
|
||||
if (pitch === undefined || pitch === null) {
|
||||
const fallbackFromRoofLines = roof?.lines?.find((l) => l?.attributes?.pitch != null)?.attributes?.pitch
|
||||
pitch = fallbackFromRoofLines
|
||||
}
|
||||
if (pitch === undefined || pitch === null) {
|
||||
const fallbackOuter = canvas
|
||||
?.getObjects()
|
||||
?.find((o) => o?.name === 'outerLine' && o?.attributes?.pitch != null)
|
||||
pitch = fallbackOuter?.attributes?.pitch
|
||||
}
|
||||
if (pitch === undefined || pitch === null) {
|
||||
pitch = 0
|
||||
}
|
||||
|
||||
|
||||
const convertedPolygon = edgeResult.Polygon?.map(point => ({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user