import { nodeShape } from './nodeStyle.mjs'
import { sideAnchorFraction } from './shapeAnchor.mjs'
import { NODE_DEFAULTS } from './defaults.mjs'
/**
* 節點某邊連接點之畫布絕對座標(邊端點與把手圓心同一基準)。
* @param {Object} node
* @param {string} side 'top'|'right'|'bottom'|'left'
* @param {Object} [nodeInternals] 量測尺寸 { width, height }(優先於 node.width/height)
* @param {Object} [defNode] 節點預設(形狀之 defNode 層, 經 nodeStyle.nodeShape 單一解析)
*/
export function getHandlePosition(node, side, nodeInternals, defNode) {
const { width: w, height: h } = resolveNodeSize(node, nodeInternals, defNode)
const f = sideAnchorFraction(nodeShape(node, defNode), side)
return { x: node.position.x + w * f.fx, y: node.position.y + h * f.fy }
}
/**
* 節點有效尺寸 —— 單一事實來源(幾何/路由/fit/形狀面/佈局共用同一優先序)。
* 優先序: 實測尺寸(live: 量測或進行中 ghost, 須為正數)→ 節點明確數值 → defNode(opt.defNodeWidth/Height)→ NODE_DEFAULTS。
* 佈局(CSS width/height)不得把實測值回寫為尺寸來源, 故呼叫端傳 live=null 即取「宣告尺寸」。
* @param {Object} node
* @param {Object|null} [live] { width, height } 實測或 ghost
* @param {Object} [defNode]
* @returns {{ width: number, height: number }}
*/
export function resolveNodeSize(node, live, defNode) {
const n = node || {}
const l = live || {}
const d = defNode || {}
const pos = (v) => (typeof v === 'number' && isFinite(v) && v > 0) ? v : null
const width = pos(l.width) || pos(n.width) || pos(d.width) || NODE_DEFAULTS.width
const height = pos(l.height) || pos(n.height) || pos(d.height) || NODE_DEFAULTS.height
return { width, height }
}
/**
* Get all nodes that overlap with a given rectangle.
*/
export function getOverlappingNodes(rect, nodes, nodeInternals, defNode) {
return nodes.filter(node => {
const { width: w, height: h } = resolveNodeSize(node, nodeInternals && nodeInternals[node.id], defNode)
const nodeRect = {
x: node.position.x,
y: node.position.y,
width: w,
height: h,
}
return rectsOverlap(rect, nodeRect)
})
}
/**
* Check if two rectangles overlap.
*/
function rectsOverlap(a, b) {
return (
a.x < b.x + b.width &&
a.x + a.width > b.x &&
a.y < b.y + b.height &&
a.y + a.height > b.y
)
}
/**
* Clamp a position within a coordinate extent.
*/
export function clampPosition(position, extent) {
if (!extent) return position
return {
x: Math.max(extent[0][0], Math.min(extent[1][0], position.x)),
y: Math.max(extent[0][1], Math.min(extent[1][1], position.y)),
}
}
/**
* Snap a position to the nearest grid point.
* @param {{ x: number, y: number }} position
* @param {number|null} gridSize - Grid cell size (single number for both axes)
*/
export function snapPosition(position, gridSize) {
if (!gridSize) return position
return {
x: Math.round(position.x / gridSize) * gridSize,
y: Math.round(position.y / gridSize) * gridSize,
}
}
/**
* 四角縮放代數(NodeWrapper 縮放手勢之純計算): 右/下角改寬高, 左/上角以對邊固定(位置隨之移動)。
* @param {'top-left'|'top-right'|'bottom-left'|'bottom-right'} edge
* @param {{x:number,y:number,width:number,height:number}} start 起始幾何
* @param {{dx:number,dy:number}} delta 畫布座標位移
* @param {{snap?:number,minSize?:number}} [opt] snap>0 時尺寸吸附格線(最小一格), 否則最小 minSize
* @returns {{width:number,height:number,x:number,y:number}}
*/
export function computeResize(edge, start, delta, opt) {
const o = opt || {}
const snap = o.snap || 0
const minSize = o.minSize || 10
const snapVal = (v) => snap ? Math.max(snap, Math.round(v / snap) * snap) : Math.max(minSize, Math.round(v))
const dx = delta.dx || 0
const dy = delta.dy || 0
let width = start.width
let height = start.height
let x = start.x
let y = start.y
const left = edge === 'top-left' || edge === 'bottom-left'
const top = edge === 'top-left' || edge === 'top-right'
if (left) {
width = snapVal(start.width - dx)
x = start.x + (start.width - width)
}
else width = snapVal(start.width + dx)
if (top) {
height = snapVal(start.height - dy)
y = start.y + (start.height - height)
}
else height = snapVal(start.height + dy)
return { width, height, x, y }
}