import { useEffect } from 'react' import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil' import { wordDisplaySelector } from '@/store/settingAtom' import { useEvent } from '@/hooks/useEvent' import { checkLineOrientation, getDistance } from '@/util/canvas-util' import { commonUtilsState, dimensionLineSettingsState } from '@/store/commonUtilsAtom' import { fontSelector } from '@/store/fontAtom' import { canvasState, currentMenuState } from '@/store/canvasAtom' import { v4 as uuidv4 } from 'uuid' import { usePopup } from '@/hooks/usePopup' import Distance from '@/components/floor-plan/modal/distance/Distance' import { usePolygon } from '@/hooks/usePolygon' import { useObjectBatch } from '@/hooks/object/useObjectBatch' import { BATCH_TYPE } from '@/common/common' export function useCommonUtils() { const canvas = useRecoilValue(canvasState) const wordDisplay = useRecoilValue(wordDisplaySelector) const { addCanvasMouseEventListener, addDocumentEventListener, initEvent, removeMouseEvent } = useEvent() const dimensionSettings = useRecoilValue(dimensionLineSettingsState) const dimensionLineTextFont = useRecoilValue(fontSelector('dimensionLineText')) const lengthTextFont = useRecoilValue(fontSelector('lengthText')) const commonTextFont = useRecoilValue(fontSelector('commonText')) const [commonUtils, setCommonUtilsState] = useRecoilState(commonUtilsState) const { addPopup, closeAll, targetClose } = usePopup() const { drawDirectionArrow, addLengthText } = usePolygon() const { applyDormers } = useObjectBatch({}) useEffect(() => { commonTextMode() if (commonUtils.dimension) { commonDimensionMode() return } if (commonUtils.distance) { commonDistanceMode() return } }, [commonUtils, dimensionSettings, commonTextFont, dimensionLineTextFont]) const commonTextMode = () => { let textbox if (commonUtils.text) { targetClose('other') setTimeout(() => { commonTextKeyEvent() addCanvasMouseEventListener('mouse:down', (event) => { const pointer = canvas?.getPointer(event.e) textbox = new fabric.Textbox('', { left: pointer.x, top: pointer.y, width: 200, editable: true, name: 'commonText', visible: wordDisplay, fill: commonTextFont.fontColor.value, fontFamily: commonTextFont.fontFamily.value, fontSize: commonTextFont.fontSize.value, fontStyle: commonTextFont.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', fontWeight: commonTextFont.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', selectable: true, lockMovementX: true, lockMovementY: true, originX: 'center', originY: 'center', }) canvas?.add(textbox) canvas.setActiveObject(textbox) textbox.enterEditing() textbox.selectAll() }) }, 100) } else { removeMouseEvent('mouse:down') const activeObject = canvas?.getActiveObject() const commonTexts = canvas?.getObjects().filter((obj) => obj.name === 'commonText') if (commonTexts) { commonTexts.forEach((text) => { if (text.text === '') { canvas?.remove(text) } }) } /*if (activeObject && activeObject.name === 'commonText') { if (activeObject && activeObject.isEditing) { if (activeObject.text === '') { canvas?.remove(activeObject) } else { activeObject.exitEditing() } //정책 협의 const texts = canvas.getObjects().filter((obj) => obj.name === 'commonText') texts.forEach((text) => { text.set({ editable: false }) }) canvas.renderAll() } }*/ initEvent() } } const createDimensionArrow = (x, y, angle, id) => { return new fabric.Triangle({ left: x, top: y, originX: 'center', originY: 'center', angle: angle, width: 15, height: 15, fill: dimensionSettings.color, selectable: true, name: 'arrow', id: id, }) } const createDimensionExtendLine = (line, lineDirection, extendLength) => { let extendLine = [] const extendLineLength = extendLength ? extendLength : 0 if (lineDirection === 'horizontal') { if (extendLineLength >= 0) { extendLine = [ [line.x1, line.y1 - 20 - extendLineLength, line.x1, line.y1 + 20], [line.x2, line.y2 - 20 - extendLineLength, line.x2, line.y2 + 20], ] } else { extendLine = [ [line.x1, line.y1 + 20 - extendLineLength, line.x1, line.y1 - 20], [line.x2, line.y2 + 20 - extendLineLength, line.x2, line.y2 - 20], ] } } else { if (extendLineLength >= 0) { extendLine = [ [line.x1 - 20 - extendLineLength, line.y1, line.x1 + 20, line.y1], [line.x2 - 20 - extendLineLength, line.y2, line.x2 + 20, line.y2], ] } else { extendLine = [ [line.x1 - 20 - extendLineLength, line.y1, line.x1 - 20, line.y1], [line.x2 - 20 - extendLineLength, line.y2, line.x2 - 20, line.y2], ] } } return extendLine } const calcDimensionPosition = (lineDirection, p1CenterX, p1CenterY, p2CenterX, p2CenterY) => { // 첫 번째 포인트에 화살표 추가 좌측 -> 우측으로 그릴때 let paddingX = lineDirection === 'horizontal' ? p1CenterX + 7.5 : p1CenterX + 0.5 let paddingX2 = lineDirection === 'horizontal' ? p2CenterX - 7.5 : p2CenterX + 0.5 let paddingY = lineDirection === 'horizontal' ? p1CenterY + 0.5 : p1CenterY + 8 let paddingY2 = lineDirection === 'horizontal' ? p2CenterY + 0.5 : p2CenterY - 8 let angle1 = lineDirection === 'horizontal' ? -90 : 0 let angle2 = lineDirection === 'horizontal' ? 90 : 180 // 우측 -> 좌측으로 그릴땐 반대 if (paddingX > paddingX2 || paddingY > paddingY2) { paddingX = lineDirection === 'horizontal' ? p1CenterX - 7.5 : p1CenterX + 0.5 paddingX2 = lineDirection === 'horizontal' ? p2CenterX + 7.5 : p2CenterX + 0.5 paddingY = lineDirection === 'horizontal' ? p1CenterY + 0.5 : p1CenterY - 7.5 paddingY2 = lineDirection === 'horizontal' ? p2CenterY + 0.5 : p2CenterY + 7.5 angle1 = lineDirection === 'horizontal' ? 90 : 180 angle2 = lineDirection === 'horizontal' ? 270 : 0 } return { paddingX, paddingX2, paddingY, paddingY2, angle1, angle2, } } const commonDimensionMode = () => { if (commonUtils.dimension) { const uuid = uuidv4() let points = [] let distanceText = null let minX, minY, maxX, maxY // 화살표를 생성하는 함수 const circleOptions = { radius: 5, strokeWidth: 2, stroke: 'red', fill: 'white', selectable: true, } const lineOptions = { stroke: dimensionSettings.color, strokeWidth: dimensionSettings.pixel, selectable: true, originX: 'center', originY: 'center', } // 캔버스에 클릭 이벤트 추가 addCanvasMouseEventListener('mouse:down', (e) => { let groupObjects = [] const pointer = canvas.getPointer(e.e) let point if (points.length === 0) { // 첫 번째 포인트는 그대로 클릭한 위치에 추가 point = new fabric.Circle({ left: pointer.x - 5, // 반지름 반영 top: pointer.y - 5, // 반지름 반영 ...circleOptions, }) points.push(point) canvas.add(point) } else if (points.length === 1) { // 두 번째 포인트는 첫 번째 포인트를 기준으로 수평 또는 수직으로만 배치 const p1 = points[0] const deltaX = Math.abs(pointer.x - (p1.left + p1.radius)) const deltaY = Math.abs(pointer.y - (p1.top + p1.radius)) if (deltaX > deltaY) { // 수평선 상에만 배치 (y 좌표 고정) point = new fabric.Circle({ left: pointer.x - 5, // 반지름 반영 top: p1.top, // y 좌표 고정 ...circleOptions, }) } else { // 수직선 상에만 배치 (x 좌표 고정) point = new fabric.Circle({ left: p1.left, // x 좌표 고정 top: pointer.y - 5, // 반지름 반영 ...circleOptions, }) } points.push(point) canvas.add(point) // 두 포인트의 중심 좌표 계산 const p2 = points[1] const p1CenterX = p1.left + p1.radius const p1CenterY = p1.top + p1.radius const p2CenterX = p2.left + p2.radius const p2CenterY = p2.top + p2.radius points.forEach((point) => { canvas?.remove(point) }) // 두 포인트 간에 직선을 그림 (중심을 기준으로) const line = new fabric.Line([p1CenterX, p1CenterY, p2CenterX, p2CenterY], { ...lineOptions, name: 'centerLine', id: uuid, }) // canvas.add(line) groupObjects.push(line) const distance = getDistance(p1CenterX, p1CenterY, p2CenterX, p2CenterY) const lineDirection = checkLineOrientation(line) const extendListArray = createDimensionExtendLine(line, lineDirection) extendListArray.forEach((line) => { const extendLine = new fabric.Line(line, { ...lineOptions, name: 'extendLine', id: uuid, }) groupObjects.push(extendLine) }) const dimensionPosition = calcDimensionPosition(lineDirection, p1CenterX, p1CenterY, p2CenterX, p2CenterY) const arrow1 = createDimensionArrow(dimensionPosition.paddingX, dimensionPosition.paddingY, dimensionPosition.angle1, uuid) // 반대 방향 화살표 const arrow2 = createDimensionArrow(dimensionPosition.paddingX2, dimensionPosition.paddingY2, dimensionPosition.angle2, uuid) // 정방향 화살표 groupObjects.push(arrow1, arrow2) distanceText = new fabric.Text(`${distance * 10} `, { left: (p1CenterX + p2CenterX) / 2 + (lineDirection === 'horizontal' ? 0 : -15), top: (p1CenterY + p2CenterY) / 2 + (lineDirection === 'horizontal' ? +15 : 0), fill: dimensionLineTextFont.fontColor.value, fontSize: dimensionLineTextFont.fontSize.value, fontFamily: dimensionLineTextFont.fontFamily.value, fontStyle: dimensionLineTextFont.fontWeight.value.toLowerCase().includes('italic') ? 'italic' : 'normal', fontWeight: dimensionLineTextFont.fontWeight.value.toLowerCase().includes('bold') ? 'bold' : 'normal', selectable: true, textAlign: 'center', originX: 'center', originY: 'center', angle: lineDirection === 'horizontal' ? 0 : 270, name: 'dimensionLineText', id: uuid, // lockMovementX: false, // lockMovementY: false, }) // canvas.add(distanceText) groupObjects.push(distanceText) const group = new fabric.Group(groupObjects, { name: 'dimensionGroup', selectable: true, originX: 'center', originY: 'center', lineDirection: lineDirection, groupId: uuid, length: distance * 10, slopeAble: false, angle1: null, angle2: null, }) canvas.add(group) // groupObjects.push(distanceText) canvas.renderAll() // 거리 계산 후, 다음 측정을 위해 초기화 points = [] if (setCommonUtilsState) setCommonUtilsState({ ...commonUtilsState, dimension: false, }) initEvent() } // 캔버스 다시 그리기 canvas.renderAll() }) } } const commonDistanceMode = () => { if (commonUtils.distance) { let points = [] let distanceText = null let drawPoints = [] const crossOptions = { stroke: 'black', strokeWidth: 1, originX: 'center', originY: 'center', name: 'distance', } const lineOptions = { stroke: 'black', strokeWidth: 1, selectable: false, strokeDashArray: [10, 5], name: 'distance', } const textOptions = { fill: 'black', fontSize: 16, selectable: true, textAlign: 'center', originX: 'center', originY: 'center', name: 'distance', } // 캔버스에 클릭 이벤트 추가 addCanvasMouseEventListener('mouse:down', function (e) { const pointer = canvas.getPointer(e.e) let point let cross = {} if (points.length === 0) { point = new fabric.Line([pointer.x - 10, pointer.y, pointer.x + 10, pointer.y], crossOptions) canvas.add(point) cross['x'] = parseInt(point.left.toFixed(0)) drawPoints.push(point) // 세로 선 생성 (십자 모양의 다른 축) point = new fabric.Line([pointer.x, pointer.y - 10, pointer.x, pointer.y + 10], crossOptions) cross['y'] = parseInt(point.top.toFixed(0)) drawPoints.push(point) canvas.add(point) points.push(cross) } else if (points.length === 1) { // 두 번째 포인트는 첫 번째 포인트를 기준으로 수평 또는 수직으로만 배치 const p1 = points[0] point = new fabric.Line([pointer.x - 10, pointer.y, pointer.x + 10, pointer.y], crossOptions) canvas.add(point) cross['x'] = parseInt(point.left.toFixed(0)) drawPoints.push(point) // 세로 선 생성 (십자 모양의 다른 축) point = new fabric.Line([pointer.x, pointer.y - 10, pointer.x, pointer.y + 10], crossOptions) canvas.add(point) cross['y'] = parseInt(point.top.toFixed(0)) drawPoints.push(point) points.push(cross) let isParallel = false if (points[0].x === points[1].x || points[0].y === points[1].y) { isParallel = true } // 두 포인트의 중심 좌표 계산 const p2 = points[1] const p1CenterX = p1.x const p1CenterY = p1.y const p2CenterX = p2.x const p2CenterY = p2.y // 두 포인트 간에 직선을 그림 (중심을 기준으로) const line = new fabric.Line([p1CenterX, p1CenterY, p2CenterX, p2CenterY], lineOptions) canvas.add(line) const distance1 = getDistance(p1CenterX, p1CenterY, p2CenterX, p2CenterY) // 거리 텍스트가 이미 있으면 업데이트하고, 없으면 새로 생성 distanceText = new fabric.Text(`${distance1 * 10}`, { left: (p1CenterX + p2CenterX) / 2, top: (p1CenterY + p2CenterY) / 2, ...textOptions, }) canvas.add(distanceText) let distance2 = 0 let distance3 = 0 if (!isParallel) { const p3 = new fabric.Point(p2CenterX, p1CenterY) const line2 = new fabric.Line([p2CenterX, p2CenterY, p3.x, p3.y], lineOptions) const line3 = new fabric.Line([p3.x, p3.y, p1CenterX, p1CenterY], lineOptions) canvas.add(line2) canvas.add(line3) distance2 = getDistance(p2CenterX, p2CenterY, p3.x, p3.y) distance3 = getDistance(p3.x, p3.y, p1CenterX, p1CenterY) distanceText = new fabric.Text(`${distance2 * 10}`, { left: (p2CenterX + p3.x) / 2, top: (p2CenterY + p3.y) / 2, ...textOptions, }) canvas.add(distanceText) distanceText = new fabric.Text(`${distance3 * 10}`, { left: (p3.x + p1CenterX) / 2, top: (p3.y + p1CenterY) / 2, ...textOptions, }) canvas.add(distanceText) } const id = uuidv4() addPopup( id, 1, , ) if (setCommonUtilsState) setCommonUtilsState({ ...commonUtils, distance: false, }) // 거리 계산 후, 다음 측정을 위해 초기화 points = [] } // 캔버스 다시 그리기 canvas.renderAll() }) } } const commonTextKeyEvent = () => { //텍스트 모드일때 엔터 이벤트 addDocumentEventListener('keydown', document, (e) => { if (e.key === 'Enter') { const activeObject = canvas.getActiveObject() if (activeObject && activeObject.name === 'commonText') { if (activeObject && activeObject.isEditing) { if (activeObject.text === '') { canvas?.remove(activeObject) } else { activeObject.exitEditing() } //정책 협의 const texts = canvas.getObjects().filter((obj) => obj.name === 'commonText') texts.forEach((text) => { text.set({ editable: false }) }) canvas.renderAll() if (setCommonUtilsState) setCommonUtilsState({ ...commonUtils, text: false }) } } initEvent() } }) } const commonFunctions = (mode) => { let tempStates = { ...commonUtils } if (tempStates[mode]) { tempStates[mode] = false } else { Object.keys(tempStates).forEach((key) => { tempStates[key] = false }) if (mode !== undefined) { tempStates[mode] = true } } if (setCommonUtilsState) setCommonUtilsState(tempStates) } const commonDeleteText = (object) => { if (object) { canvas?.remove(object) if (object.id) { const group = canvas.getObjects().filter((obj) => obj.id === object.id) group.forEach((obj) => canvas?.remove(obj)) } if (object.type === 'group') { object._objects.forEach((obj) => { if (obj.hasOwnProperty('texts')) { obj.texts.forEach((text) => { canvas?.remove(text) }) } }) } else { if (object.hasOwnProperty('texts')) { object.texts.forEach((text) => { canvas?.remove(text) }) } } } } const commonMoveObject = (obj) => { if (obj) { obj.set({ lockMovementX: false, lockMovementY: false, }) const originLeft = obj.left const originTop = obj.top addCanvasMouseEventListener('mouse:up', (e) => { obj.set({ lockMovementX: true, lockMovementY: true, }) initEvent() obj.setCoords() updateGroupObjectCoords(obj, originLeft, originTop) canvas?.renderAll() }) } } const commonCopyObject = (obj) => { if (obj) { //도머는 복사하기 보다 새로 호출해서 만드는 방법으로 함 if (obj.name == BATCH_TYPE.TRIANGLE_DORMER || obj.name == BATCH_TYPE.PENTAGON_DORMER) { //그룹 객체 0번에 도머 속성을 넣어둠 const dormerAttributes = obj._objects[0].dormerAttributes const dormerName = obj._objects[0].name const dormerParams = { height: dormerAttributes.height, width: dormerAttributes.width, pitch: dormerAttributes.pitch, offsetRef: dormerAttributes.offsetRef, offsetWidthRef: dormerAttributes.offsetWidthRef, directionRef: dormerAttributes.directionRef, } const buttonAct = dormerName == BATCH_TYPE.TRIANGLE_DORMER ? 3 : 4 applyDormers(dormerParams, buttonAct) } else { let clonedObj = null obj.clone((cloned) => { clonedObj = cloned clonedObj.fontSize = lengthTextFont.fontSize.value }) addCanvasMouseEventListener('mouse:move', (e) => { const pointer = canvas?.getPointer(e.e) if (!clonedObj) return canvas .getObjects() .filter((obj) => obj.name === 'clonedObj') .forEach((obj) => canvas?.remove(obj)) clonedObj.set({ left: pointer.x, top: pointer.y, name: 'clonedObj', }) canvas.add(clonedObj) }) addCanvasMouseEventListener('mouse:down', (e) => { clonedObj.set({ lockMovementX: true, lockMovementY: true, name: obj.name, editable: false, id: uuidv4(), //복사된 객체라 새로 따준다 }) //객체가 그룹일 경우에는 그룹 아이디를 따로 넣어준다 if (clonedObj.type === 'group') clonedObj.set({ groupId: uuidv4() }) //배치면일 경우 if (obj.name === 'roof') { clonedObj.setCoords() clonedObj.fire('modified') clonedObj.fire('polygonMoved') clonedObj.set({ direction: obj.direction, directionText: obj.directionText, roofMaterial: obj.roofMaterial }) obj.lines.forEach((line, index) => { clonedObj.lines[index].set({ attributes: line.attributes }) }) canvas.renderAll() addLengthText(clonedObj) //수치 추가 drawDirectionArrow(clonedObj) //방향 화살표 추가 } initEvent() }) } } } const editText = () => { const obj = canvas?.getActiveObject() obj.set({ editable: true }) obj.enterEditing() commonTextKeyEvent() } const deleteObject = () => { const selectedObj = canvas?.getActiveObjects() if (selectedObj) { selectedObj.forEach((obj) => { commonDeleteText(obj) }) } selectedObj.forEach((obj) => { if (obj.type === 'group') { obj._objects.forEach((lines) => { if (lines.hasOwnProperty('arrow')) { canvas .getObjects() .filter((obj1) => obj1.name === 'arrow' && lines.id === obj1.parentId) .forEach((arrow) => { canvas?.remove(arrow) }) } }) } }) } const moveObject = () => { const obj = canvas?.getActiveObject() commonMoveObject(obj) } const copyObject = () => { const obj = canvas?.getActiveObject() commonCopyObject(obj) } const closeDistancePopup = () => { const obj = canvas?.getObjects().filter((obj) => obj.name === 'distance') if (obj) canvas.remove(...obj) initEvent() } //선택된 그룹객체 restore 하고 item으로 다시 그리고 그 그린 객체 가지고 수정해서 재그룹화 시킨다 const changeDimensionExtendLine = () => { const group = canvas?.getActiveObject() const restoreGroup = group._restoreObjectsState() canvas?.remove(group) canvas?.renderAll() restoreGroup._objects.forEach((obj) => { canvas?.add(obj) }) const id = group.groupId const originLineDirection = group.lineDirection const textObj = canvas?.getObjects().filter((obj) => obj.name === 'dimensionLineText' && obj.id === id)[0] const centerLine = canvas?.getObjects().filter((obj) => obj.name === 'centerLine' && obj.id === id)[0] const extendLine = canvas?.getObjects().filter((obj) => obj.name === 'extendLine' && obj.id === id) const arrows = canvas?.getObjects().filter((obj) => obj.name === 'arrow' && obj.id === id) const originX = centerLine.x1 const originY = centerLine.y1 let reGroupObj = [] addCanvasMouseEventListener('mouse:down', (e) => { const pointer = canvas?.getPointer(e.e) if (originLineDirection === 'horizontal') { centerLine.set({ x1: centerLine.x1, y1: pointer.y, x2: centerLine.x2, y2: pointer.y, }) const differenceY = centerLine.y1 - originY extendLine.forEach((obj) => { obj.set({ x1: obj.x1, y1: originY, x2: obj.x2, y2: differenceY > 0 ? pointer.y + 20 : pointer.y - 20, }) }) arrows.forEach((arrow) => { arrow.set({ top: pointer.y, }) }) textObj.set({ top: pointer.y + 15, }) textObj.setCoords() } else { centerLine.set({ x1: pointer.x, y1: centerLine.y1, x2: pointer.x, y2: centerLine.y2, }) const differenceX = centerLine.x1 - originX extendLine.forEach((obj) => { obj.set({ x1: originX, y1: obj.y1, x2: differenceX > 0 ? pointer.x + 20 : pointer.x - 20, y2: obj.y2, }) }) arrows.forEach((arrow) => { arrow.set({ left: pointer.x, }) }) textObj.set({ left: pointer.x - 15, }) textObj.setCoords() } reGroupObj.push(centerLine, ...extendLine, ...arrows, textObj) canvas?.remove(centerLine, ...extendLine, ...arrows, textObj) const reGroup = new fabric.Group(reGroupObj, { name: 'dimensionGroup', selectable: true, lineDirection: originLineDirection, groupId: id, }) reGroupObj = [] canvas.add(reGroup) initEvent() }) } // 그룹 이동 시 라인 및 각 객체의 좌표를 절대 좌표로 업데이트하는 함수 function updateGroupObjectCoords(targetObj, originLeft, originTop) { const diffrenceLeft = targetObj.left - originLeft const diffrenceTop = targetObj.top - originTop if (targetObj.type === 'group') { targetObj.getObjects().forEach((obj) => { // 그룹 내 객체의 절대 좌표를 계산 const originObjLeft = obj.left const originObjTop = obj.top if (obj.type === 'line') { // Line 객체의 경우, x1, y1, x2, y2 절대 좌표 계산 obj.set({ x1: obj.x1 + diffrenceLeft, y1: obj.y1 + diffrenceTop, x2: obj.x2 + diffrenceLeft, y2: obj.y2 + diffrenceTop, }) obj.set({ left: originObjLeft, top: originObjTop, }) obj.fire('modified') } else { // 다른 객체의 경우 left, top 절대 좌표 설정 obj.set({ left: obj.left, top: obj.top, }) obj.fire('modified') } obj.setCoords() // 좌표 반영 }) } else { if (targetObj.type === 'line') { const originObjLeft = obj.left const originObjTop = obj.top if (obj.type === 'line') { // Line 객체의 경우, x1, y1, x2, y2 절대 좌표 계산 obj.set({ x1: obj.x1 + diffrenceLeft, y1: obj.y1 + diffrenceTop, x2: obj.x2 + diffrenceLeft, y2: obj.y2 + diffrenceTop, }) obj.set({ left: originObjLeft, top: originObjTop, }) obj.fire('modified') } else { targetObj.set({ ...targetObj, left: targetObj.left, top: targetObj.top, }) obj.fire('modified') } targetObj.setCoords() } canvas?.renderAll() } } const deleteOuterLineObject = () => { const selectedOuterLine = canvas.getActiveObject() if (selectedOuterLine && selectedOuterLine.name === 'outerLine') { canvas.remove(selectedOuterLine) canvas.renderAll() } } return { commonFunctions, dimensionSettings, commonMoveObject, commonDeleteText, deleteObject, moveObject, copyObject, editText, changeDimensionExtendLine, deleteOuterLineObject, } }