js/nodeStyle.mjs

/**
 * 節點樣式之共用解析(NodeWrapper 與 NodeBody/把手共用同一基準)
 */
import { sideAnchorFraction } from './shapeAnchor.mjs'
import { resolveNodeSize } from './geometry.mjs'

export const SHAPES = ['rectangle', 'diamond', 'ellipse', 'triangle-up', 'triangle-right', 'triangle-down', 'triangle-left']

/**
 * 節點有效形狀(單一來源): node.shape → defNode.shape → 'rectangle'; 非法值視為未給。
 * 節點面(NodeFace)、外框判定、把手佈局(NodePorts)、邊端點(geometry)皆須經此解析, 否則 defNodeShape 設定時各層分家。
 */
export function nodeShape(node, defNode) {
    const n = (node && node.shape) || ''
    if (SHAPES.indexOf(n) >= 0) return n
    const d = (defNode && defNode.shape) || ''
    if (SHAPES.indexOf(d) >= 0) return d
    return 'rectangle'
}

export function isTriangleShape(shape) {
    return shape === 'triangle-up' || shape === 'triangle-right' || shape === 'triangle-down' || shape === 'triangle-left'
}

export function isSvgShape(node, defNode) {
    const s = nodeShape(node, defNode)
    return s !== 'rectangle'
}

/**
 * 節點外框(CSS border)寬度(px): 矩形=節點/預設之 edgeWidth; SVG 形狀(菱形/橢圓/三角)之外框由 SVG stroke 繪製,
 * 容器本身 border 為 0——使 padding box 與外框盒重合, 形狀 stroke、把手圓心、連線端點三者同以外框盒為基準
 */
export function nodeBorderWidth(node, defNode) {
    if (isSvgShape(node, defNode)) return 0
    const d = defNode || {}
    if (node && node.edgeWidth !== undefined) return Number(node.edgeWidth) || 0
    if (d.edgeWidth !== undefined) return Number(d.edgeWidth) || 0
    return 1
}

/**
 * 把手樣式(CSS 變數): 由 defNode 之 handle* 解析, 供 Handle 以 inline style 注入
 * --vf-hs 尺寸 / --vf-hface 面色 / --vf-hedge 框線色 / --vf-hew 框線寬
 */
export function handleStyleVars(defNode) {
    const d = defNode || {}
    return {
        '--vf-hs': (d.handleSize !== undefined && d.handleSize !== null ? d.handleSize : 10) + 'px',
        '--vf-hface': d.handleFaceColor || '#555555',
        '--vf-hedge': d.handleEdgeColor || '#ffffff',
        '--vf-hew': (d.handleEdgeWidth !== undefined && d.handleEdgeWidth !== null ? d.handleEdgeWidth : 1) + 'px',
    }
}

/**
 * 把手定位(inline style): 圓心 = 節點外框盒上 sideAnchorFraction 所指之點(與 geometry.getHandlePosition 同一 fraction)。
 * 把手為節點 padding box 內之絕對定位元素, 以百分比取 fraction, 落在外框盒邊上者再外推節點外框寬(nodeBorder);
 * translate(-50%,-50%) 置中, 故 hover 放大時圓心不動。
 */
export function handlePlacementStyle(shape, side, nodeBorder) {
    const f = sideAnchorFraction(shape, side)
    const b = Number(nodeBorder) || 0
    const px = (v) => `${(v * 100).toFixed(4).replace(/\.?0+$/, '')}%`
    const shift = (v) => (v === 0 ? -b : (v === 1 ? b : 0))
    const left = shift(f.fx) ? `calc(${px(f.fx)} + ${shift(f.fx)}px)` : px(f.fx)
    const top = shift(f.fy) ? `calc(${px(f.fy)} + ${shift(f.fy)}px)` : px(f.fy)
    return { left, top, transform: 'translate(-50%, -50%)' }
}

/**
 * 三角形節點之文字位移(往形狀重心方向): 使 label 落在三角形內部而非外接矩形中心
 */
export function labelOffsetStyle(node, defNode) {
    const s = nodeShape(node, defNode)
    if (!isTriangleShape(s)) return null
    const { width: w, height: h } = resolveNodeSize(node, null, defNode)
    let x = 0
    let y = 0
    if (s === 'triangle-right') x = Math.round(-w / 6)
    else if (s === 'triangle-down') y = Math.round(-h / 6)
    else if (s === 'triangle-left') x = Math.round(w / 6)
    else y = Math.round(h / 6)
    return { transform: `translate(${x}px, ${y}px)` }
}