dsh-drawai 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +208 -42
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -3523,6 +3523,104 @@ window.__ModuleLoader__.load({
|
|
|
3523
3523
|
* 3. **进出形状的最后一段垂直于落点所在的边**(从左边进就横着进)。
|
|
3524
3524
|
* 否则线会贴着边框斜切进去,看起来像画错了。
|
|
3525
3525
|
*/
|
|
3526
|
+
/** 修复手动路径的一跳:保留两端必经点,在节点边界外的正交网格上寻路。
|
|
3527
|
+
* 正常的 L 形不进此分支;方向作为状态,禁止原路折返,代价先比转弯再比长度。 */
|
|
3528
|
+
function avoidOrthogonalLeg(a, b, boxes, before, next, sourceSide, targetSide) {
|
|
3529
|
+
const obstacles = Array.isArray(boxes) ? boxes : []
|
|
3530
|
+
const xs = new Set([a.x, b.x, a.x - EDGE_GRID, a.x + EDGE_GRID, b.x - EDGE_GRID, b.x + EDGE_GRID])
|
|
3531
|
+
const ys = new Set([a.y, b.y, a.y - EDGE_GRID, a.y + EDGE_GRID, b.y - EDGE_GRID, b.y + EDGE_GRID])
|
|
3532
|
+
for (const box of obstacles) {
|
|
3533
|
+
xs.add(Math.floor((box.geo.x - 8) / EDGE_GRID) * EDGE_GRID)
|
|
3534
|
+
xs.add(Math.ceil((box.geo.x + box.geo.w + 8) / EDGE_GRID) * EDGE_GRID)
|
|
3535
|
+
ys.add(Math.floor((box.geo.y - 8) / EDGE_GRID) * EDGE_GRID)
|
|
3536
|
+
ys.add(Math.ceil((box.geo.y + box.geo.h + 8) / EDGE_GRID) * EDGE_GRID)
|
|
3537
|
+
}
|
|
3538
|
+
const xx = [...xs].sort((x, y) => x - y)
|
|
3539
|
+
const yy = [...ys].sort((x, y) => x - y)
|
|
3540
|
+
const width = xx.length
|
|
3541
|
+
const pointAt = (v) => ({ x: xx[v % width], y: yy[Math.floor(v / width)] })
|
|
3542
|
+
// e/s/w/n;相反方向差 2。
|
|
3543
|
+
const direction = (p, q) => p.x === q.x ? (q.y > p.y ? 1 : 3) : (q.x > p.x ? 0 : 2)
|
|
3544
|
+
const sideDirection = { e: 0, s: 1, w: 2, n: 3 }
|
|
3545
|
+
const start = yy.indexOf(a.y) * width + xx.indexOf(a.x)
|
|
3546
|
+
const goal = yy.indexOf(b.y) * width + xx.indexOf(b.x)
|
|
3547
|
+
const incoming = before === null ? 4 : direction(before, a)
|
|
3548
|
+
const outgoing = next !== null && (next.x === b.x || next.y === b.y) && (next.x !== b.x || next.y !== b.y) ? direction(b, next) : 4
|
|
3549
|
+
const costs = new Map()
|
|
3550
|
+
const parents = new Map()
|
|
3551
|
+
const heap = []
|
|
3552
|
+
const better = (x, y) => x.bends < y.bends || (x.bends === y.bends && x.length < y.length)
|
|
3553
|
+
const push = (item) => {
|
|
3554
|
+
heap.push(item)
|
|
3555
|
+
let i = heap.length - 1
|
|
3556
|
+
while (i > 0) {
|
|
3557
|
+
const parent = (i - 1) >> 1
|
|
3558
|
+
if (!better(item, heap[parent])) break
|
|
3559
|
+
heap[i] = heap[parent]
|
|
3560
|
+
i = parent
|
|
3561
|
+
}
|
|
3562
|
+
heap[i] = item
|
|
3563
|
+
}
|
|
3564
|
+
const pop = () => {
|
|
3565
|
+
const first = heap[0]
|
|
3566
|
+
const last = heap.pop()
|
|
3567
|
+
if (heap.length > 0) {
|
|
3568
|
+
let i = 0
|
|
3569
|
+
while (i * 2 + 1 < heap.length) {
|
|
3570
|
+
let child = i * 2 + 1
|
|
3571
|
+
if (child + 1 < heap.length && better(heap[child + 1], heap[child])) child += 1
|
|
3572
|
+
if (!better(heap[child], last)) break
|
|
3573
|
+
heap[i] = heap[child]
|
|
3574
|
+
i = child
|
|
3575
|
+
}
|
|
3576
|
+
heap[i] = last
|
|
3577
|
+
}
|
|
3578
|
+
return first
|
|
3579
|
+
}
|
|
3580
|
+
const initial = { key: start * 5 + incoming, bends: 0, length: 0 }
|
|
3581
|
+
costs.set(initial.key, initial)
|
|
3582
|
+
push(initial)
|
|
3583
|
+
const blocked = new Map()
|
|
3584
|
+
while (heap.length > 0) {
|
|
3585
|
+
const cur = pop()
|
|
3586
|
+
if (costs.get(cur.key) !== cur) continue
|
|
3587
|
+
const v = Math.floor(cur.key / 5)
|
|
3588
|
+
const dir = cur.key % 5
|
|
3589
|
+
if (v === goal) {
|
|
3590
|
+
const path = []
|
|
3591
|
+
let key = cur.key
|
|
3592
|
+
while (key !== undefined) {
|
|
3593
|
+
path.push(pointAt(Math.floor(key / 5)))
|
|
3594
|
+
key = parents.get(key)
|
|
3595
|
+
}
|
|
3596
|
+
return simplifyCollinear(path.reverse())
|
|
3597
|
+
}
|
|
3598
|
+
const x = v % width
|
|
3599
|
+
const y = Math.floor(v / width)
|
|
3600
|
+
const neighbors = [x + 1 < width ? v + 1 : -1, y + 1 < yy.length ? v + width : -1, x > 0 ? v - 1 : -1, y > 0 ? v - width : -1]
|
|
3601
|
+
const p = pointAt(v)
|
|
3602
|
+
for (let d = 0; d < 4; d += 1) {
|
|
3603
|
+
const nv = neighbors[d]
|
|
3604
|
+
if (nv < 0 || (dir < 4 && (dir + 2) % 4 === d)) continue
|
|
3605
|
+
if (v === start && sourceSide && d !== sideDirection[sourceSide]) continue
|
|
3606
|
+
if (nv === goal && targetSide && d !== (sideDirection[targetSide] + 2) % 4) continue
|
|
3607
|
+
if (nv === goal && outgoing < 4 && (d + 2) % 4 === outgoing) continue
|
|
3608
|
+
const q = pointAt(nv)
|
|
3609
|
+
const edgeKey = Math.min(v, nv) + ':' + Math.max(v, nv)
|
|
3610
|
+
if (!blocked.has(edgeKey)) blocked.set(edgeKey, obstacles.some((box) => segmentHitsBox(p, q, box.geo, 0)))
|
|
3611
|
+
if (blocked.get(edgeKey)) continue
|
|
3612
|
+
const candidate = { key: nv * 5 + d, bends: cur.bends + (dir < 4 && dir !== d ? 1 : 0), length: cur.length + Math.abs(q.x - p.x) + Math.abs(q.y - p.y) }
|
|
3613
|
+
const known = costs.get(candidate.key)
|
|
3614
|
+
if (known !== undefined && !better(candidate, known)) continue
|
|
3615
|
+
costs.set(candidate.key, candidate)
|
|
3616
|
+
parents.set(candidate.key, cur.key)
|
|
3617
|
+
push(candidate)
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
// 节点互相覆盖、必经点不可达时保留原路径,不能让连线消失。
|
|
3621
|
+
return null
|
|
3622
|
+
}
|
|
3623
|
+
|
|
3526
3624
|
function routeThroughWaypoints(fromBox, toBox, waypoints, obstacleBoxes, userWaypoints) {
|
|
3527
3625
|
// 折点可能落在某个节点**内部**(用户拖到节点上、或节点移动后把它吞进去了)。
|
|
3528
3626
|
// 照直连过去线就穿进节点里,所以先推到外面 —— 只改路由用的副本,不动用户数据。
|
|
@@ -3541,14 +3639,30 @@ window.__ModuleLoader__.load({
|
|
|
3541
3639
|
// 最后的落点先算出来(它只取决于最后一个折点):它也是最后一个折点的"下一站",
|
|
3542
3640
|
// 传给 connectOrtho 用于判断"到站方向会不会被下一段顶回来"。
|
|
3543
3641
|
const end = borderPointToward(toBox.geo, safe[safe.length - 1])
|
|
3642
|
+
const append = (target, next, sourceSide, targetSide) => {
|
|
3643
|
+
const offset = pts.length - 1
|
|
3644
|
+
const before = offset > 0 ? pts[offset - 1] : null
|
|
3645
|
+
axis = connectOrtho(pts, prev, target, axis, next)
|
|
3646
|
+
const leg = pts.slice(offset)
|
|
3647
|
+
const test = (before === null ? [] : [before]).concat(leg, next === null ? [] : [next])
|
|
3648
|
+
const hits = leg.some((p, i) => i > 0 && Array.isArray(obstacleBoxes) && obstacleBoxes.some((box) => segmentHitsBox(leg[i - 1], p, box.geo, 0)))
|
|
3649
|
+
const fold = test.some((p, i) => i > 1 && isFoldApex(test[i - 2], test[i - 1], p))
|
|
3650
|
+
const wrongSide = leg.length > 1 && ((sourceSide && !leavesSide(leg[0], leg[1], sourceSide)) || (targetSide && !leavesSide(leg[leg.length - 1], leg[leg.length - 2], targetSide)))
|
|
3651
|
+
if (Array.isArray(obstacleBoxes) && leg.length > 1 && (hits || fold || wrongSide)) {
|
|
3652
|
+
const repaired = avoidOrthogonalLeg(prev, target, obstacleBoxes, before, next, sourceSide, targetSide)
|
|
3653
|
+
if (repaired !== null) pts.splice(offset + 1, pts.length - offset - 1, ...repaired.slice(1))
|
|
3654
|
+
}
|
|
3655
|
+
prev = target
|
|
3656
|
+
}
|
|
3544
3657
|
for (let i = 0; i < safe.length; i += 1) {
|
|
3545
3658
|
const next = i + 1 < safe.length ? safe[i + 1] : end
|
|
3546
|
-
|
|
3547
|
-
|
|
3659
|
+
const atEnd = i === safe.length - 1 && Math.abs(safe[i].x - end.x) < 0.5 && Math.abs(safe[i].y - end.y) < 0.5
|
|
3660
|
+
append(safe[i], next, pts.length === 1 ? start.side : null, atEnd ? end.side : null)
|
|
3548
3661
|
}
|
|
3549
3662
|
// 落点在左右边 → 最后一段要横着进 → 先竖后横,所以传 'v';上下边反之。
|
|
3550
3663
|
const approach = end.side === 'e' || end.side === 'w' ? 'v' : 'h'
|
|
3551
|
-
|
|
3664
|
+
axis = approach
|
|
3665
|
+
append(end, null, pts.length === 1 ? start.side : null, end.side)
|
|
3552
3666
|
return pts
|
|
3553
3667
|
}
|
|
3554
3668
|
|
|
@@ -3559,6 +3673,14 @@ window.__ModuleLoader__.load({
|
|
|
3559
3673
|
return finalizePath(build(start), safe, keep)
|
|
3560
3674
|
}
|
|
3561
3675
|
|
|
3676
|
+
/** 从节点侧面沿外法线离开,禁止贴边走或反向穿进节点。 */
|
|
3677
|
+
function leavesSide(a, b, side) {
|
|
3678
|
+
if (side === 'e') return Math.abs(a.y - b.y) < 0.5 && b.x > a.x
|
|
3679
|
+
if (side === 'w') return Math.abs(a.y - b.y) < 0.5 && b.x < a.x
|
|
3680
|
+
if (side === 's') return Math.abs(a.x - b.x) < 0.5 && b.y > a.y
|
|
3681
|
+
return Math.abs(a.x - b.x) < 0.5 && b.y < a.y
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3562
3684
|
/** 从 safe(已推出的折点序列,含桩点)里挑出"对应人摆的那些点"。
|
|
3563
3685
|
*
|
|
3564
3686
|
* 按**坐标**配对而不是按下标:桩点与折点在 chain 里混在一起,下标要另算一遍首尾桩点个数,
|
|
@@ -3613,12 +3735,23 @@ window.__ModuleLoader__.load({
|
|
|
3613
3735
|
|
|
3614
3736
|
/** 在第 segmentIndex 段上插新折点时,它排在 points[] 的第几位:数该段起点之前有几个折点。 */
|
|
3615
3737
|
function insertIndexForPath(pts, waypoints, segmentIndex) {
|
|
3616
|
-
|
|
3738
|
+
// 共线化简后,已有折点可能位于线段内部,不能只查路径顶点。
|
|
3739
|
+
const position = (point) => {
|
|
3740
|
+
for (let i = 0; i < pts.length - 1; i += 1) {
|
|
3741
|
+
const a = pts[i]
|
|
3742
|
+
const b = pts[i + 1]
|
|
3743
|
+
const length = Math.abs(b.x - a.x) + Math.abs(b.y - a.y)
|
|
3744
|
+
const before = Math.abs(point.x - a.x) + Math.abs(point.y - a.y)
|
|
3745
|
+
const after = Math.abs(point.x - b.x) + Math.abs(point.y - b.y)
|
|
3746
|
+
if (length > 0 && Math.abs(before + after - length) < 0.5) return i + before / length
|
|
3747
|
+
}
|
|
3748
|
+
return -1
|
|
3749
|
+
}
|
|
3617
3750
|
for (let i = 0; i < waypoints.length; i += 1) {
|
|
3618
|
-
const at =
|
|
3619
|
-
if (at
|
|
3751
|
+
const at = position(waypoints[i])
|
|
3752
|
+
if (at > segmentIndex + 1) return i
|
|
3620
3753
|
}
|
|
3621
|
-
return
|
|
3754
|
+
return waypoints.length
|
|
3622
3755
|
}
|
|
3623
3756
|
|
|
3624
3757
|
/**
|
|
@@ -3637,6 +3770,53 @@ window.__ModuleLoader__.load({
|
|
|
3637
3770
|
return at
|
|
3638
3771
|
}
|
|
3639
3772
|
|
|
3773
|
+
/** 被化简到段内部的折点也属于整段,必须和两个端点使用同一位移。 */
|
|
3774
|
+
function movePinnedSegment(points, a, b, move) {
|
|
3775
|
+
return points.map((p, i) => ({
|
|
3776
|
+
x: p.x + (i >= a && i <= b ? move.x : 0),
|
|
3777
|
+
y: p.y + (i >= a && i <= b ? move.y : 0),
|
|
3778
|
+
}))
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
/** 段拖动的快照。真实连接端点属于节点,不作为随段移动的用户折点。
|
|
3782
|
+
* 首末段在引出点处截断:平移后只需一条横向/纵向连接,避免先抵达边框再倒回桩点。 */
|
|
3783
|
+
function prepareSegmentDrag(doc, edge, pts, index) {
|
|
3784
|
+
if (pts === null || index < 0 || index >= pts.length - 1) return null
|
|
3785
|
+
const path = pts.map((p) => ({ x: p.x, y: p.y }))
|
|
3786
|
+
let points = Array.isArray(edge.points) ? edge.points.map((p) => ({ x: p.x, y: p.y })) : []
|
|
3787
|
+
const geometry = buildGeometry(doc)
|
|
3788
|
+
const sides = sidesFromStyle(edge.style)
|
|
3789
|
+
const trim = (at, neighbor, end) => {
|
|
3790
|
+
// 悬空端维持原有语义;只有真实节点上的连接端点需要保留固定引出段。
|
|
3791
|
+
const id = end === 'source' ? edge.from : edge.to
|
|
3792
|
+
const box = geometry.byId[id]
|
|
3793
|
+
if (box === undefined) return
|
|
3794
|
+
const border = path[at]
|
|
3795
|
+
const other = path[neighbor]
|
|
3796
|
+
const length = Math.abs(other.x - border.x) + Math.abs(other.y - border.y)
|
|
3797
|
+
if (length < 0.5) return
|
|
3798
|
+
const side = sides[end] || borderPointToward(box.geo, other).side
|
|
3799
|
+
if (!leavesSide(border, other, side)) return
|
|
3800
|
+
const jetty = Number.isFinite(sides.jetty) && sides.jetty > 0 ? sides.jetty : 24
|
|
3801
|
+
const inset = Math.min(jetty, length)
|
|
3802
|
+
const proxy = { x: border.x + (other.x - border.x) * inset / length, y: border.y + (other.y - border.y) * inset / length }
|
|
3803
|
+
// 去掉固定引出段内部的旧折点(包含过去误钉下的边框端点),不让它们再随段移动。
|
|
3804
|
+
points = points.filter((p) => {
|
|
3805
|
+
if (Math.abs(p.x - proxy.x) < 0.5 && Math.abs(p.y - proxy.y) < 0.5) return true
|
|
3806
|
+
const distance = Math.abs(p.x - border.x) + Math.abs(p.y - border.y)
|
|
3807
|
+
const rest = Math.abs(p.x - proxy.x) + Math.abs(p.y - proxy.y)
|
|
3808
|
+
return Math.abs(distance + rest - inset) >= 0.5
|
|
3809
|
+
})
|
|
3810
|
+
path[at] = proxy
|
|
3811
|
+
}
|
|
3812
|
+
if (index === 0) trim(0, 1, 'source')
|
|
3813
|
+
if (index === path.length - 2) trim(path.length - 1, path.length - 2, 'target')
|
|
3814
|
+
const a = ensurePinned(path, points, index)
|
|
3815
|
+
const b = ensurePinned(path, points, index + 1)
|
|
3816
|
+
if (a < 0 || b < 0) return null
|
|
3817
|
+
return { base: points, a: a, b: b, horizontal: Math.abs(pts[index].y - pts[index + 1].y) < 0.5 }
|
|
3818
|
+
}
|
|
3819
|
+
|
|
3640
3820
|
/**
|
|
3641
3821
|
* 连线路径的唯一入口。
|
|
3642
3822
|
*
|
|
@@ -3901,7 +4081,17 @@ window.__ModuleLoader__.load({
|
|
|
3901
4081
|
const index = applyRouteHysteresis(raw, picked, hint)
|
|
3902
4082
|
// 自动候选理论上不会折返,这里照样过一遍:折返在画面上只有坏处(重合线 + 尖点鼓包),
|
|
3903
4083
|
// 而"每条边上都没有折返"是能在自测里断言的硬不变量。
|
|
3904
|
-
|
|
4084
|
+
const path = simplifyCollinear(removeRetraces(dedupePoints(raw[index].points)))
|
|
4085
|
+
// 六个固定候选不一定覆盖可行通道;不能在它们全被挡住时直接选一条穿模的线。
|
|
4086
|
+
if (path.some((p, i) => i > 0 && boxes.some((box) => segmentHitsBox(path[i - 1], p, box.geo, 0)))) {
|
|
4087
|
+
const start = path[0]
|
|
4088
|
+
const end = path[path.length - 1]
|
|
4089
|
+
const sourceSide = from.w > 0 && from.h > 0 ? borderPointToward(from, start).side : null
|
|
4090
|
+
const targetSide = to.w > 0 && to.h > 0 ? borderPointToward(to, end).side : null
|
|
4091
|
+
const repaired = avoidOrthogonalLeg(start, end, boxes, null, null, sourceSide, targetSide)
|
|
4092
|
+
if (repaired !== null) return repaired
|
|
4093
|
+
}
|
|
4094
|
+
return path
|
|
3905
4095
|
}
|
|
3906
4096
|
|
|
3907
4097
|
|
|
@@ -10446,16 +10636,7 @@ window.__ModuleLoader__.load({
|
|
|
10446
10636
|
for (let i = 0; i < next.edges.length; i += 1) {
|
|
10447
10637
|
const e = next.edges[i]
|
|
10448
10638
|
if (e.id !== edgeDrag.edgeId) continue
|
|
10449
|
-
|
|
10450
|
-
if (edgeDrag.a < points.length) {
|
|
10451
|
-
points[edgeDrag.a].x += moveX
|
|
10452
|
-
points[edgeDrag.a].y += moveY
|
|
10453
|
-
}
|
|
10454
|
-
if (edgeDrag.b < points.length) {
|
|
10455
|
-
points[edgeDrag.b].x += moveX
|
|
10456
|
-
points[edgeDrag.b].y += moveY
|
|
10457
|
-
}
|
|
10458
|
-
e.points = points
|
|
10639
|
+
e.points = movePinnedSegment(edgeDrag.base, edgeDrag.a, edgeDrag.b, { x: moveX, y: moveY })
|
|
10459
10640
|
break
|
|
10460
10641
|
}
|
|
10461
10642
|
}, 'segment:' + edgeDrag.edgeId + ':' + edgeDrag.a + ':' + edgeDrag.b)
|
|
@@ -11304,39 +11485,22 @@ window.__ModuleLoader__.load({
|
|
|
11304
11485
|
return
|
|
11305
11486
|
}
|
|
11306
11487
|
|
|
11307
|
-
const points = Array.isArray(edge.points) ? edge.points.slice() : []
|
|
11308
11488
|
if (kind === 'segment') {
|
|
11309
11489
|
// 拖动某一段 = **整段平移**(draw.io 的段把手语义),而不是插入一个自由折点。
|
|
11310
11490
|
//
|
|
11311
|
-
//
|
|
11312
|
-
// 之后同时平移这两个折点,这一段就整体挪走了。
|
|
11313
|
-
//
|
|
11314
|
-
// 端点落在起点/终点一侧时同样成立:那个顶点被钉住之后,形状的落点会朝它重新计算,
|
|
11315
|
-
// 于是自动多出一小段连接 —— 也就是"相当于新建一条线段"。
|
|
11491
|
+
// 连接端点保持固定,只钉住可移动区间;按下仅保存快照,实际移动后才改文档。
|
|
11316
11492
|
const pts = edgeRoutePoints(current, edge, routeMemoryRef.current)
|
|
11317
|
-
|
|
11318
|
-
|
|
11319
|
-
const b = pts[index + 1]
|
|
11320
|
-
const pinA = ensurePinned(pts, points, index)
|
|
11321
|
-
const pinB = ensurePinned(pts, points, index + 1)
|
|
11322
|
-
if (pinA < 0 || pinB < 0 || pinA === pinB) return
|
|
11323
|
-
const saved = points.map((p) => ({ x: p.x, y: p.y }))
|
|
11324
|
-
applyLocal((next) => {
|
|
11325
|
-
for (let i = 0; i < next.edges.length; i += 1) {
|
|
11326
|
-
if (next.edges[i].id !== edgeId) continue
|
|
11327
|
-
next.edges[i].points = saved.map((p) => ({ x: p.x, y: p.y }))
|
|
11328
|
-
break
|
|
11329
|
-
}
|
|
11330
|
-
})
|
|
11493
|
+
const seed = prepareSegmentDrag(current, edge, pts, index)
|
|
11494
|
+
if (seed === null) return
|
|
11331
11495
|
const origin = toUserSpace(event)
|
|
11332
11496
|
if (origin === null) return
|
|
11333
11497
|
edgeDragRef.current = {
|
|
11334
11498
|
edgeId: edgeId,
|
|
11335
11499
|
kind: 'segment',
|
|
11336
|
-
a:
|
|
11337
|
-
b:
|
|
11338
|
-
horizontal:
|
|
11339
|
-
base:
|
|
11500
|
+
a: seed.a,
|
|
11501
|
+
b: seed.b,
|
|
11502
|
+
horizontal: seed.horizontal,
|
|
11503
|
+
base: seed.base,
|
|
11340
11504
|
originX: origin.x,
|
|
11341
11505
|
originY: origin.y,
|
|
11342
11506
|
}
|
|
@@ -15420,6 +15584,8 @@ window.__ModuleLoader__.load({
|
|
|
15420
15584
|
drawaiPaneOf: drawaiPaneOf,
|
|
15421
15585
|
clearStaleTextSelection: clearStaleTextSelection,
|
|
15422
15586
|
ensurePinned: ensurePinned,
|
|
15587
|
+
movePinnedSegment: movePinnedSegment,
|
|
15588
|
+
prepareSegmentDrag: prepareSegmentDrag,
|
|
15423
15589
|
SIDES: SIDES,
|
|
15424
15590
|
HOT_PAD: HOT_PAD,
|
|
15425
15591
|
GRID: GRID,
|