@operato/scene-storage 10.0.0-beta.41 → 10.0.0-beta.42
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/CHANGELOG.md +12 -0
- package/MIGRATION-plan-a-slot-api.md +266 -0
- package/PLAN-A-rack-as-slot-holder.md +164 -0
- package/dist/crane.js +1 -1
- package/dist/crane.js.map +1 -1
- package/dist/index.d.ts +3 -4
- package/dist/index.js +1 -2
- package/dist/index.js.map +1 -1
- package/dist/rack-grid-3d.d.ts +18 -7
- package/dist/rack-grid-3d.js +372 -69
- package/dist/rack-grid-3d.js.map +1 -1
- package/dist/rack-grid-cell.d.ts +21 -72
- package/dist/rack-grid-cell.js +147 -243
- package/dist/rack-grid-cell.js.map +1 -1
- package/dist/rack-grid.d.ts +277 -56
- package/dist/rack-grid.js +1230 -695
- package/dist/rack-grid.js.map +1 -1
- package/dist/rack-materials.d.ts +9 -0
- package/dist/rack-materials.js +55 -0
- package/dist/rack-materials.js.map +1 -0
- package/dist/storage-rack-3d.d.ts +15 -0
- package/dist/storage-rack-3d.js +131 -30
- package/dist/storage-rack-3d.js.map +1 -1
- package/dist/storage-rack.d.ts +242 -45
- package/dist/storage-rack.js +684 -106
- package/dist/storage-rack.js.map +1 -1
- package/package.json +3 -3
- package/src/crane.ts +1 -1
- package/src/index.ts +3 -4
- package/src/rack-grid-3d.ts +383 -80
- package/src/rack-grid-cell.ts +161 -305
- package/src/rack-grid.ts +1263 -762
- package/src/rack-materials.ts +61 -0
- package/src/storage-rack-3d.ts +144 -30
- package/src/storage-rack.ts +763 -111
- package/test/test-carrier-lifecycle.ts +361 -0
- package/test/test-coord-alignment.ts +201 -0
- package/test/test-external-to-rack.ts +461 -0
- package/test/test-mover-concurrent-bug.ts +304 -0
- package/test/test-mover-rollback.ts +290 -0
- package/test/test-r19-place-absorb.ts +174 -0
- package/test/test-rack-3d-attach-real.ts +301 -0
- package/test/test-rack-concurrent.ts +254 -0
- package/test/test-rack-edge-cases.ts +323 -0
- package/test/test-rack-grid-cell.ts +318 -0
- package/test/test-rack-grid-location.ts +657 -0
- package/test/test-real-3d-positioning.ts +158 -0
- package/test/test-slot-center-convention.ts +116 -0
- package/test/test-slot-target.ts +189 -0
- package/test/test-storage-rack-batched.ts +606 -0
- package/test/test-storage-rack-click.ts +329 -0
- package/test/test-storage-rack-slot-api.ts +357 -0
- package/test/test-toscene-convention.ts +162 -0
- package/test/test-user-scenario-sequential.ts +334 -0
- package/translations/en.json +2 -0
- package/translations/ja.json +2 -0
- package/translations/ko.json +2 -0
- package/translations/ms.json +2 -0
- package/translations/zh.json +2 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/rack-column.d.ts +0 -35
- package/dist/rack-column.js +0 -258
- package/dist/rack-column.js.map +0 -1
- package/dist/rack-grid-helpers.d.ts +0 -28
- package/dist/rack-grid-helpers.js +0 -71
- package/dist/rack-grid-helpers.js.map +0 -1
- package/dist/rack-grid-location.d.ts +0 -37
- package/dist/rack-grid-location.js +0 -227
- package/dist/rack-grid-location.js.map +0 -1
- package/dist/storage-cell-3d.d.ts +0 -25
- package/dist/storage-cell-3d.js +0 -88
- package/dist/storage-cell-3d.js.map +0 -1
- package/dist/storage-cell.d.ts +0 -73
- package/dist/storage-cell.js +0 -215
- package/dist/storage-cell.js.map +0 -1
- package/src/rack-column.ts +0 -340
- package/src/rack-grid-helpers.ts +0 -77
- package/src/rack-grid-location.ts +0 -286
- package/src/storage-cell-3d.ts +0 -101
- package/src/storage-cell.ts +0 -267
- package/test/test-cell-position.ts +0 -105
- package/test/test-rack-grid.ts +0 -77
package/dist/rack-grid.js
CHANGED
|
@@ -1,200 +1,82 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
1
2
|
/*
|
|
2
3
|
* Copyright © HatioLab Inc. All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* RackGrid — *grid layout 의 컨테이너*.
|
|
6
|
+
*
|
|
7
|
+
* 각 (col, row) 위치마다 *하나의 자식 컴포넌트* 보유 가능:
|
|
8
|
+
* - StorageRack (한 column 의 vertical stack — stock 보관)
|
|
9
|
+
* - Crane / AGV / forklift / 미니로드 등 *operation archetype* (이동/작업 설비)
|
|
10
|
+
* - 또는 비어 있음 (= 통로 / 작업자 공간 / 무엇이든)
|
|
11
|
+
*
|
|
12
|
+
* 외부에는 *location string* (예: "Z-A01-03-1") 으로 API 노출. 내부 cellId 와 양방향
|
|
13
|
+
* 변환은 RackGrid 가 책임.
|
|
14
|
+
*
|
|
15
|
+
* **Performance** — 각 자식 StorageRack 이 자체 InstancedMesh 로 stock 렌더링.
|
|
16
|
+
* storage-rack 의 batched 시각화 성능 그대로 계승. RackGrid 는 *얇은 wrapper*.
|
|
17
|
+
*
|
|
18
|
+
* **Transfer 컨트랙** — SlottedHolder 인터페이스 구현. 내부적으로 *자식 StorageRack*
|
|
19
|
+
* 으로 위임. Mover / Crane 등이 *RackGrid 의 slot target* 으로 인터랙트 가능.
|
|
20
|
+
*
|
|
21
|
+
* **Configuration 생산성** — rack-table 의 *location 룰* (locPattern, sectionDigits 등)
|
|
22
|
+
* 차용. cell-component 없이 *sparse cellOverrides* 만으로 동일 UX 제공.
|
|
23
|
+
*
|
|
24
|
+
* **Aisle/Empty** — `cellOverrides[posKey].isEmpty = true` 로 명시. 의미는
|
|
25
|
+
* *location 미부여 + increaseLocation 의 aisle row 인식* 만. 그 위치의 자식 컴포넌트
|
|
26
|
+
* 종류는 *자유* (Crane/AGV/forklift/그냥 비움 모두 가능).
|
|
27
|
+
*
|
|
28
|
+
* **CellId 컨벤션** (내부):
|
|
29
|
+
* - posKey = `${col-1}-${row-1}` (0-based, 2 segments) — *grid 평면 위치*
|
|
30
|
+
* - cellId = `${col-1}-${row-1}-${shelf-1}` (0-based, 3 segments) — *grid + shelf*
|
|
31
|
+
* - 자식 StorageRack 내부에서는 `0-0-${shelf-1}` (자체 cellId 형식). RackGrid 가 변환.
|
|
3
32
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
33
|
+
import { ContainerAbstract, Layout, Model, sceneComponent } from '@hatiolab/things-scene';
|
|
34
|
+
import * as THREE from 'three';
|
|
35
|
+
import { Placeable, SlotTarget } from '@operato/scene-base';
|
|
6
36
|
import { RackGrid3D } from './rack-grid-3d.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
type: 'number',
|
|
15
|
-
label: 'rows',
|
|
16
|
-
name: 'rows',
|
|
17
|
-
property: 'rows'
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
type: 'number',
|
|
21
|
-
label: 'columns',
|
|
22
|
-
name: 'columns',
|
|
23
|
-
property: 'columns'
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
type: 'string',
|
|
27
|
-
label: 'zone',
|
|
28
|
-
name: 'zone',
|
|
29
|
-
property: 'zone'
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
type: 'number',
|
|
33
|
-
label: 'shelves',
|
|
34
|
-
name: 'shelves',
|
|
35
|
-
property: 'shelves'
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
type: 'number',
|
|
39
|
-
label: 'depth',
|
|
40
|
-
name: 'depth',
|
|
41
|
-
property: 'depth'
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
type: 'string',
|
|
45
|
-
label: 'location-pattern',
|
|
46
|
-
name: 'locPattern',
|
|
47
|
-
placeholder: '{z}{s}-{u}-{sh}'
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
type: 'number',
|
|
51
|
-
label: 'section-digits',
|
|
52
|
-
name: 'sectionDigits',
|
|
53
|
-
placeholder: '1, 2, 3, ...'
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
type: 'number',
|
|
57
|
-
label: 'unit-digits',
|
|
58
|
-
name: 'unitDigits',
|
|
59
|
-
placeholder: '1, 2, 3, ...'
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
type: 'string',
|
|
63
|
-
label: 'shelf-locations',
|
|
64
|
-
name: 'shelfLocations',
|
|
65
|
-
placeholder: '1,2,3,... / ,,,04'
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
type: 'number',
|
|
69
|
-
label: 'stock-scale',
|
|
70
|
-
name: 'stockScale',
|
|
71
|
-
property: {
|
|
72
|
-
step: 0.01,
|
|
73
|
-
min: 0,
|
|
74
|
-
max: 1
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
type: 'checkbox',
|
|
79
|
-
label: 'hide-rack-frame',
|
|
80
|
-
name: 'hideRackFrame'
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
type: 'id-input',
|
|
84
|
-
label: 'legend-target',
|
|
85
|
-
name: 'legendTarget',
|
|
86
|
-
property: {
|
|
87
|
-
component: 'legend'
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
{
|
|
91
|
-
type: 'checkbox',
|
|
92
|
-
label: 'hide-empty-stock',
|
|
93
|
-
name: 'hideEmptyStock'
|
|
94
|
-
}
|
|
95
|
-
],
|
|
96
|
-
help: 'scene/component/rack-grid'
|
|
97
|
-
};
|
|
98
|
-
const SIDES = {
|
|
99
|
-
all: ['top', 'left', 'bottom', 'right'],
|
|
100
|
-
out: ['top', 'left', 'bottom', 'right'],
|
|
101
|
-
left: ['left'],
|
|
102
|
-
right: ['right'],
|
|
103
|
-
top: ['top'],
|
|
104
|
-
bottom: ['bottom'],
|
|
105
|
-
leftright: ['left', 'right'],
|
|
106
|
-
topbottom: ['top', 'bottom']
|
|
107
|
-
};
|
|
108
|
-
const CLEAR_STYLE = {
|
|
109
|
-
strokeStyle: '',
|
|
110
|
-
lineDash: 'solid',
|
|
111
|
-
lineWidth: 0
|
|
112
|
-
};
|
|
113
|
-
const DEFAULT_STYLE = {
|
|
114
|
-
strokeStyle: '#999',
|
|
115
|
-
lineDash: 'solid',
|
|
116
|
-
lineWidth: 1
|
|
117
|
-
};
|
|
118
|
-
const TABLE_LAYOUT = Layout.get('table');
|
|
37
|
+
// RackGridCell 의 refid 충돌 회피 — load 시 동적 생성되는 cell 의 refid 가 *부모
|
|
38
|
+
// RackGrid + 모델 안 다른 컴포넌트* refid 와 겹치지 않도록 *높은 시작값 + monotonic
|
|
39
|
+
// counter*. hierarchy override 가 save 시 refid 제거 → 모델 파일엔 안 새겨짐.
|
|
40
|
+
let _cellRefidCounter = 100_000_000;
|
|
41
|
+
function _nextCellRefid() {
|
|
42
|
+
return _cellRefidCounter++;
|
|
43
|
+
}
|
|
119
44
|
function buildNewCell(app) {
|
|
120
|
-
|
|
45
|
+
const refid = _nextCellRefid();
|
|
46
|
+
const m = Model.compile({
|
|
121
47
|
type: 'rack-grid-cell',
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
left: 0,
|
|
125
|
-
top: 0,
|
|
126
|
-
width: 1,
|
|
127
|
-
height: 1,
|
|
128
|
-
textWrap: true,
|
|
129
|
-
isEmpty: false,
|
|
130
|
-
border: buildBorderStyle(DEFAULT_STYLE, 'all')
|
|
48
|
+
refid,
|
|
49
|
+
left: 0, top: 0, width: 1, height: 1, isEmpty: false
|
|
131
50
|
}, app);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return (SIDES[where] || []).reduce((border, side) => {
|
|
140
|
-
border[side] = style;
|
|
141
|
-
return border;
|
|
142
|
-
}, {});
|
|
143
|
-
}
|
|
144
|
-
function setCellBorder(cell, style, where) {
|
|
145
|
-
if (!cell) {
|
|
146
|
-
return;
|
|
51
|
+
// Model.compile 이 refid 를 자동 generator 로 덮어쓸 수 있어 *강제 재 set*.
|
|
52
|
+
try {
|
|
53
|
+
if (m.refid !== refid) {
|
|
54
|
+
m.refid = refid;
|
|
55
|
+
m.state && (m.state.refid = refid);
|
|
56
|
+
m._state && (m._state.refid = refid);
|
|
57
|
+
}
|
|
147
58
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
function isLeftMost(total, columns, indices, i) {
|
|
151
|
-
return i == 0 || !(i % columns) || indices.indexOf(i - 1) == -1;
|
|
152
|
-
}
|
|
153
|
-
function isRightMost(total, columns, indices, i) {
|
|
154
|
-
return i == total - 1 || i % columns == columns - 1 || indices.indexOf(i + 1) == -1;
|
|
155
|
-
}
|
|
156
|
-
function isTopMost(total, columns, indices, i) {
|
|
157
|
-
return i < columns || indices.indexOf(i - columns) == -1;
|
|
158
|
-
}
|
|
159
|
-
function isBottomMost(total, columns, indices, i) {
|
|
160
|
-
return i > total - columns - 1 || indices.indexOf(i + columns) == -1;
|
|
59
|
+
catch { }
|
|
60
|
+
return m;
|
|
161
61
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
function below(columns, i) {
|
|
166
|
-
return i + columns;
|
|
167
|
-
}
|
|
168
|
-
function before(columns, i) {
|
|
169
|
-
return !(i % columns) ? -1 : i - 1;
|
|
170
|
-
}
|
|
171
|
-
function after(columns, i) {
|
|
172
|
-
return !((i + 1) % columns) ? -1 : i + 1;
|
|
173
|
-
}
|
|
174
|
-
function array(value, size) {
|
|
62
|
+
const TABLE_LAYOUT = Layout.get('table');
|
|
63
|
+
function arrayOf(value, size) {
|
|
175
64
|
const arr = [];
|
|
176
65
|
for (let i = 0; i < size; i++)
|
|
177
|
-
arr.push(
|
|
66
|
+
arr.push(value);
|
|
178
67
|
return arr;
|
|
179
68
|
}
|
|
180
69
|
const columnControlHandler = {
|
|
181
|
-
ondragmove
|
|
70
|
+
ondragmove(point, index, component) {
|
|
182
71
|
const { left, top, width, height } = component.textBounds;
|
|
183
72
|
const widths_sum = component.widths_sum;
|
|
184
73
|
const widths = component.widths.slice();
|
|
185
|
-
|
|
186
|
-
const origin_pos_unit = widths.slice(0, index + 1).reduce((sum, width) => sum + width, 0);
|
|
74
|
+
const origin_pos_unit = widths.slice(0, index + 1).reduce((sum, w) => sum + w, 0);
|
|
187
75
|
const origin_offset = left + (origin_pos_unit / widths_sum) * width;
|
|
188
|
-
/*
|
|
189
|
-
* point의 좌표는 부모 레이어 기준의 x, y 값이다.
|
|
190
|
-
* 따라서, 도형의 회전을 감안한 좌표로의 변환이 필요하다.
|
|
191
|
-
* Transcoord시에는 point좌표가 부모까지 transcoord되어있는 상태이므로,
|
|
192
|
-
* 컴포넌트자신에 대한 transcoord만 필요하다.(마지막 파라미터를 false로).
|
|
193
|
-
*/
|
|
194
76
|
const transcoorded = component.transcoordP2S(point.x, point.y);
|
|
195
77
|
const diff = transcoorded.x - origin_offset;
|
|
196
78
|
let diff_unit = (diff / width) * widths_sum;
|
|
197
|
-
const min_width_unit = (widths_sum / width) * 5;
|
|
79
|
+
const min_width_unit = (widths_sum / width) * 5;
|
|
198
80
|
if (diff_unit < 0)
|
|
199
81
|
diff_unit = -Math.min(widths[index] - min_width_unit, -diff_unit);
|
|
200
82
|
else
|
|
@@ -205,24 +87,17 @@ const columnControlHandler = {
|
|
|
205
87
|
}
|
|
206
88
|
};
|
|
207
89
|
const rowControlHandler = {
|
|
208
|
-
ondragmove
|
|
90
|
+
ondragmove(point, index, component) {
|
|
209
91
|
const { left, top, width, height } = component.textBounds;
|
|
210
92
|
const heights_sum = component.heights_sum;
|
|
211
93
|
const heights = component.heights.slice();
|
|
212
|
-
/* 컨트롤의 원래 위치를 구한다. */
|
|
213
94
|
index -= component.columns - 1;
|
|
214
|
-
const origin_pos_unit = heights.slice(0, index + 1).reduce((sum,
|
|
95
|
+
const origin_pos_unit = heights.slice(0, index + 1).reduce((sum, h) => sum + h, 0);
|
|
215
96
|
const origin_offset = top + (origin_pos_unit / heights_sum) * height;
|
|
216
|
-
/*
|
|
217
|
-
* point의 좌표는 부모 레이어 기준의 x, y 값이다.
|
|
218
|
-
* 따라서, 도형의 회전을 감안한 좌표로의 변환이 필요하다.
|
|
219
|
-
* Transcoord시에는 point좌표가 부모까지 transcoord되어있는 상태이므로,
|
|
220
|
-
* 컴포넌트자신에 대한 transcoord만 필요하다.(마지막 파라미터를 false로).
|
|
221
|
-
*/
|
|
222
97
|
const transcoorded = component.transcoordP2S(point.x, point.y);
|
|
223
98
|
const diff = transcoorded.y - origin_offset;
|
|
224
99
|
let diff_unit = (diff / height) * heights_sum;
|
|
225
|
-
const min_height_unit = (heights_sum / height) * 5;
|
|
100
|
+
const min_height_unit = (heights_sum / height) * 5;
|
|
226
101
|
if (diff_unit < 0)
|
|
227
102
|
diff_unit = -Math.min(heights[index] - min_height_unit, -diff_unit);
|
|
228
103
|
else
|
|
@@ -232,598 +107,1258 @@ const rowControlHandler = {
|
|
|
232
107
|
component.set('heights', heights);
|
|
233
108
|
}
|
|
234
109
|
};
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
get
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
110
|
+
// ─── Nature ───────────────────────────────────────────────────────────────────
|
|
111
|
+
//
|
|
112
|
+
// `nature` 는 *static const* 가 아니라 *get nature()* — properties 의 widget event
|
|
113
|
+
// handler 안 `this` 가 *해당 RackGrid 인스턴스* 를 가리키도록 (화살표 함수가
|
|
114
|
+
// getter 의 `this` 를 capture). rack-table-cell 의 동일 패턴.
|
|
115
|
+
// ─── RackGrid ─────────────────────────────────────────────────────────────────
|
|
116
|
+
let RackGrid = class RackGrid extends Placeable(ContainerAbstract) {
|
|
117
|
+
static placement = 'floor';
|
|
118
|
+
static align = 'bottom';
|
|
119
|
+
static defaultDepth = (h) => h.ceiling - h.floor;
|
|
120
|
+
get nature() {
|
|
121
|
+
return {
|
|
122
|
+
mutable: false,
|
|
123
|
+
resizable: true,
|
|
124
|
+
rotatable: true,
|
|
125
|
+
properties: [
|
|
126
|
+
{ type: 'number', label: 'columns', name: 'columns' },
|
|
127
|
+
{ type: 'number', label: 'rows', name: 'rows' },
|
|
128
|
+
{ type: 'number', label: 'shelves', name: 'shelves' },
|
|
129
|
+
{ type: 'number', label: 'shelf-base-height', name: 'shelfBaseHeight' },
|
|
130
|
+
{ type: 'string', label: 'zone', name: 'zone' },
|
|
131
|
+
{ type: 'string', label: 'location-pattern', name: 'locPattern', placeholder: '{z}{s}-{u}-{sh}' },
|
|
132
|
+
{ type: 'number', label: 'section-digits', name: 'sectionDigits' },
|
|
133
|
+
{ type: 'number', label: 'unit-digits', name: 'unitDigits' },
|
|
134
|
+
{ type: 'string', label: 'shelf-locations', name: 'shelfLocations', placeholder: '1,2,3 또는 ,,,04' },
|
|
135
|
+
{
|
|
136
|
+
// root.selected 의 RackGridCell 들 또는 *전체 grid* 에 채번 적용.
|
|
137
|
+
type: 'location-increase-pattern',
|
|
138
|
+
label: '',
|
|
139
|
+
name: '',
|
|
140
|
+
property: {
|
|
141
|
+
event: {
|
|
142
|
+
'increase-location-pattern': (event) => {
|
|
143
|
+
const { increasingDirection, skipNumbering, startSection, startUnit } = event.detail;
|
|
144
|
+
const selected = this.root?.selected;
|
|
145
|
+
const keys = (selected || [])
|
|
146
|
+
.filter(c => c?.state?.type === 'rack-grid-cell')
|
|
147
|
+
.map(c => c.state.cellId)
|
|
148
|
+
.filter((k) => !!k);
|
|
149
|
+
this.increaseLocation(keys, increasingDirection, skipNumbering, startSection, startUnit);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{ type: 'checkbox', label: 'hide-rack-frame', name: 'hideRackFrame' },
|
|
155
|
+
{ type: 'checkbox', label: 'hide-horizontal-frame', name: 'hideHorizontalFrame' },
|
|
156
|
+
{ type: 'id-input', label: 'legend-target', name: 'legendTarget', property: { component: 'legend' } },
|
|
157
|
+
{ type: 'checkbox', label: 'hide-empty-stock', name: 'hideEmptyStock' },
|
|
158
|
+
{ type: 'id-input', label: 'popup-ref', name: 'popupRef', property: { component: 'popup' } }
|
|
159
|
+
],
|
|
160
|
+
help: 'scene/component/rack-grid'
|
|
161
|
+
};
|
|
272
162
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
this._stock_materials = [];
|
|
276
|
-
delete this._default_material;
|
|
277
|
-
delete this._empty_material;
|
|
163
|
+
get anchors() {
|
|
164
|
+
return [];
|
|
278
165
|
}
|
|
279
|
-
|
|
280
|
-
|
|
166
|
+
/**
|
|
167
|
+
* Serialize 시 자식 RackGridCell 의 redundant 속성 제거:
|
|
168
|
+
* - 좌표/변환 (left/top/width/height/zPos/rotation/scale/translate) — table layout 이
|
|
169
|
+
* runtime 에 자동 결정. state 저장하면 layout 결과와 불일치 위험.
|
|
170
|
+
* - **refid** — *동적 생성* 되는 자식이라 *부모의 refid 가 그대로 들어가면 충돌*
|
|
171
|
+
* ("Refid Index replaced" 경고). 자식의 refid 는 *load 시 새로 자동 부여*. (외부
|
|
172
|
+
* 참조용 *id* 는 사용자 명시 시만 유지)
|
|
173
|
+
*
|
|
174
|
+
* 유지: cellId, section, unit, isEmpty, border, shelfLocations 등 *own 데이터*.
|
|
175
|
+
*/
|
|
176
|
+
get hierarchy() {
|
|
177
|
+
const base = super.hierarchy;
|
|
178
|
+
if (base?.components && Array.isArray(base.components)) {
|
|
179
|
+
base.components = base.components.map((c) => {
|
|
180
|
+
if (!c || typeof c !== 'object')
|
|
181
|
+
return c;
|
|
182
|
+
const { left, top, width, height, zPos, rotation, scale, translate, refid, ...rest } = c;
|
|
183
|
+
return rest;
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return base;
|
|
281
187
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
this._resetMaterials();
|
|
286
|
-
super.dispose();
|
|
287
|
-
delete this._focused_cell;
|
|
188
|
+
/** Focusible — children (RackGridCell) 도 editor 의 selection 가능 (rack-table 동일). */
|
|
189
|
+
get focusible() {
|
|
190
|
+
return false;
|
|
288
191
|
}
|
|
192
|
+
/** Lifecycle — children 자동 생성. columns × rows 만큼 RackGridCell 자식. */
|
|
289
193
|
created() {
|
|
290
|
-
const tobeSize = this.
|
|
291
|
-
const gap = this.size() - tobeSize;
|
|
292
|
-
if (gap
|
|
194
|
+
const tobeSize = this.columns * this.rackRows;
|
|
195
|
+
const gap = (this.size?.() ?? this.components.length) - tobeSize;
|
|
196
|
+
if (gap === 0) {
|
|
197
|
+
// 자식 수는 맞음 — cellId 만 sync
|
|
198
|
+
this._syncChildCellIds();
|
|
293
199
|
return;
|
|
294
200
|
}
|
|
295
201
|
else if (gap > 0) {
|
|
296
|
-
|
|
297
|
-
this.remove(
|
|
202
|
+
;
|
|
203
|
+
this.remove?.(this.components.slice(gap));
|
|
298
204
|
}
|
|
299
205
|
else {
|
|
300
|
-
|
|
206
|
+
const newbies = [];
|
|
301
207
|
for (let i = 0; i < -gap; i++)
|
|
302
208
|
newbies.push(buildNewCell(this.app));
|
|
303
|
-
this.add(newbies);
|
|
209
|
+
this.add?.(newbies);
|
|
304
210
|
}
|
|
305
211
|
const widths = this.getState('widths');
|
|
306
212
|
const heights = this.getState('heights');
|
|
307
213
|
if (!widths || widths.length < this.columns)
|
|
308
214
|
this.set('widths', this.widths);
|
|
309
|
-
if (!heights || heights.length < this.
|
|
215
|
+
if (!heights || heights.length < this.rackRows)
|
|
310
216
|
this.set('heights', this.heights);
|
|
217
|
+
this._syncChildCellIds();
|
|
311
218
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
219
|
+
/** columns/rows 변경 시 children 재구성. rack-table.buildCells 패턴. */
|
|
220
|
+
onchange(after, _before) {
|
|
221
|
+
super.onchange?.(after, _before);
|
|
222
|
+
if ('columns' in after || 'rows' in after) {
|
|
223
|
+
const oldCols = _before.columns ?? this.columns;
|
|
224
|
+
const oldRows = _before.rows ?? this.rackRows;
|
|
225
|
+
this._buildCells(this.rackRows, this.columns, oldRows, oldCols);
|
|
226
|
+
}
|
|
227
|
+
// 룰 관련 변경 → location 역인덱스 무효화
|
|
228
|
+
if ('locPattern' in after || 'sectionDigits' in after || 'unitDigits' in after ||
|
|
229
|
+
'shelfLocations' in after || 'zone' in after || 'shelves' in after) {
|
|
230
|
+
this.invalidateLocationIndex();
|
|
231
|
+
}
|
|
232
|
+
// stock 시각 영향 속성 → InstancedMesh 재빌드
|
|
233
|
+
if ('data' in after || 'hideEmptyStock' in after || 'shelves' in after ||
|
|
234
|
+
'shelfBaseHeight' in after || 'width' in after || 'depth' in after ||
|
|
235
|
+
'height' in after || 'columns' in after || 'rows' in after) {
|
|
236
|
+
;
|
|
237
|
+
this._realObject?.rebuildStockMesh?.();
|
|
238
|
+
}
|
|
239
|
+
// hideRackFrame / hideHorizontalFrame 변경 → frame visibility 즉시 토글
|
|
240
|
+
if ('hideRackFrame' in after || 'hideHorizontalFrame' in after) {
|
|
241
|
+
;
|
|
242
|
+
this._realObject?.applyFrameVisibility?.();
|
|
243
|
+
}
|
|
315
244
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (widths.length < this.columns)
|
|
321
|
-
return widths.concat(array(1, this.columns - widths.length));
|
|
322
|
-
else if (widths.length > this.columns)
|
|
323
|
-
return widths.slice(0, this.columns);
|
|
324
|
-
return widths;
|
|
245
|
+
/** state.data 변경 시 호출 (things-scene 의 onchange<PropName>). */
|
|
246
|
+
onchangeData() {
|
|
247
|
+
;
|
|
248
|
+
this._realObject?.rebuildStockMesh?.();
|
|
325
249
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
250
|
+
/** state.data 의 records — Plan A 의 stock 보관소. */
|
|
251
|
+
get records() {
|
|
252
|
+
return this.state.data ?? [];
|
|
253
|
+
}
|
|
254
|
+
// ── Legend integration ──────────────────────────────────
|
|
255
|
+
_legendTarget;
|
|
256
|
+
/**
|
|
257
|
+
* Legend 컴포넌트 lookup. 우선순위:
|
|
258
|
+
* 1) state.legendTarget id 명시
|
|
259
|
+
* 2) scene 전체 의 type='legend' 첫 번째 컴포넌트 (자동 발견)
|
|
260
|
+
*/
|
|
261
|
+
get legendTarget() {
|
|
262
|
+
if (this._legendTarget)
|
|
263
|
+
return this._legendTarget;
|
|
264
|
+
const id = this.state.legendTarget;
|
|
265
|
+
if (id) {
|
|
266
|
+
const found = this.root?.findById?.(id);
|
|
267
|
+
if (found) {
|
|
268
|
+
this._legendTarget = found;
|
|
269
|
+
found.on?.('change', this._onLegendChanged, this);
|
|
270
|
+
return found;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const visit = (node) => {
|
|
274
|
+
if (!node)
|
|
275
|
+
return undefined;
|
|
276
|
+
if (node.state?.type === 'legend')
|
|
277
|
+
return node;
|
|
278
|
+
const children = node.components;
|
|
279
|
+
if (children) {
|
|
280
|
+
for (const c of children) {
|
|
281
|
+
const r = visit(c);
|
|
282
|
+
if (r)
|
|
283
|
+
return r;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return undefined;
|
|
287
|
+
};
|
|
288
|
+
const found = visit(this.root);
|
|
289
|
+
if (found) {
|
|
290
|
+
this._legendTarget = found;
|
|
291
|
+
found.on?.('change', this._onLegendChanged, this);
|
|
292
|
+
}
|
|
293
|
+
return found;
|
|
294
|
+
}
|
|
295
|
+
_onLegendChanged = () => {
|
|
296
|
+
;
|
|
297
|
+
this._realObject?.rebuildStockMesh?.();
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* record 의 legend.field 값을 ranges 와 매칭해 색상 해석.
|
|
301
|
+
* - `range.value === recordValue` (카테고리 일치)
|
|
302
|
+
* - `range.min ≤ Number(v) < range.max` (수치 범위)
|
|
303
|
+
* - 매칭 없으면 `defaultColor` 또는 undefined
|
|
304
|
+
*/
|
|
305
|
+
// ── View-mode click → rack-grid-cell-click event + popup ──
|
|
306
|
+
get eventMap() {
|
|
307
|
+
return {
|
|
308
|
+
'(self)': {
|
|
309
|
+
'(self)': {
|
|
310
|
+
click: this._onViewClick
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
};
|
|
335
314
|
}
|
|
336
|
-
|
|
315
|
+
_onViewClick = (mouseEvent) => {
|
|
316
|
+
if (!this.app?.isViewMode)
|
|
317
|
+
return;
|
|
318
|
+
const ro = this._realObject;
|
|
319
|
+
if (!ro?.object3d)
|
|
320
|
+
return;
|
|
321
|
+
const hit = this._raycastHit(mouseEvent);
|
|
322
|
+
if (!hit)
|
|
323
|
+
return;
|
|
324
|
+
const stockMesh = ro.stockMesh;
|
|
325
|
+
let cellId;
|
|
326
|
+
let record;
|
|
327
|
+
let isStock = false;
|
|
328
|
+
let instanceId;
|
|
329
|
+
if (hit.object === stockMesh) {
|
|
330
|
+
const records = stockMesh.userData?._records;
|
|
331
|
+
record = records?.[hit.instanceId ?? -1];
|
|
332
|
+
cellId = record?.cellId;
|
|
333
|
+
isStock = true;
|
|
334
|
+
instanceId = hit.instanceId;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
const cid = this._cellIdFromWorldPoint(hit.point);
|
|
338
|
+
if (!cid || cid.startsWith('out-of-bounds'))
|
|
339
|
+
return;
|
|
340
|
+
cellId = cid;
|
|
341
|
+
const data = this.state.data;
|
|
342
|
+
record = Array.isArray(data) ? data.find(r => r?.cellId === cid) : undefined;
|
|
343
|
+
}
|
|
344
|
+
const payload = { cellId, record, hitPoint: hit.point, instanceId, isStock };
|
|
345
|
+
this.trigger('rack-grid-cell-click', payload);
|
|
346
|
+
this._invokePopup(cellId, record);
|
|
347
|
+
// popup 외부 click 으로 인식되어 자동 close 되는 회귀 차단
|
|
348
|
+
mouseEvent.stopPropagation?.();
|
|
349
|
+
};
|
|
350
|
+
/** state.popupRef Popup 컴포넌트 invoke. anchor = 클릭된 cell 의 SlotTarget. */
|
|
351
|
+
_invokePopup(cellId, record) {
|
|
352
|
+
const popupRefId = this.state.popupRef;
|
|
353
|
+
if (!popupRefId || !cellId)
|
|
354
|
+
return;
|
|
355
|
+
const popupComp = this.root?.findById?.(popupRefId);
|
|
356
|
+
if (!popupComp || typeof popupComp.openPopup !== 'function') {
|
|
357
|
+
console.warn(`[rack-grid] popupRef="${popupRefId}" 가 가리키는 컴포넌트 없거나 openPopup 미지원`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const anchor = this.slotTargetAt(cellId);
|
|
361
|
+
popupComp.openPopup(record ?? { cellId }, { anchor });
|
|
362
|
+
}
|
|
363
|
+
/** raycast → 우리 RackGrid 의 어떤 mesh 가 closest hit 인지. */
|
|
364
|
+
_raycastHit(mouseEvent) {
|
|
365
|
+
const ro = this._realObject;
|
|
366
|
+
if (!ro?.object3d)
|
|
367
|
+
return undefined;
|
|
368
|
+
const tc = ro.threeContainer;
|
|
369
|
+
if (!tc)
|
|
370
|
+
return undefined;
|
|
371
|
+
const cap = tc._threeCapability ?? tc._capability;
|
|
372
|
+
let intersects;
|
|
373
|
+
if (cap?.getObjectsByRaycast) {
|
|
374
|
+
intersects = cap.getObjectsByRaycast();
|
|
375
|
+
}
|
|
376
|
+
if (!intersects || intersects.length === 0) {
|
|
377
|
+
const scene = tc.scene3d;
|
|
378
|
+
const renderer = tc.renderer3d;
|
|
379
|
+
const camera = tc.activeCamera3d ??
|
|
380
|
+
cap?.activeCamera ??
|
|
381
|
+
cap?.camera;
|
|
382
|
+
const canvas = renderer?.domElement;
|
|
383
|
+
if (!scene || !canvas || !camera)
|
|
384
|
+
return undefined;
|
|
385
|
+
const rect = canvas.getBoundingClientRect();
|
|
386
|
+
if (rect.width === 0 || rect.height === 0)
|
|
387
|
+
return undefined;
|
|
388
|
+
const ndc = new THREE.Vector2(((mouseEvent.clientX - rect.left) / rect.width) * 2 - 1, -((mouseEvent.clientY - rect.top) / rect.height) * 2 + 1);
|
|
389
|
+
const raycaster = new THREE.Raycaster();
|
|
390
|
+
raycaster.setFromCamera(ndc, camera);
|
|
391
|
+
intersects = raycaster.intersectObjects(scene.children, true);
|
|
392
|
+
}
|
|
393
|
+
if (!intersects || intersects.length === 0)
|
|
394
|
+
return undefined;
|
|
395
|
+
const closest = intersects[0];
|
|
396
|
+
let obj = closest.object;
|
|
397
|
+
while (obj) {
|
|
398
|
+
if (obj.userData?.context === ro)
|
|
399
|
+
return closest;
|
|
400
|
+
obj = obj.parent;
|
|
401
|
+
}
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
/** world point → cellId (col-row-shelf) 역산. */
|
|
405
|
+
_cellIdFromWorldPoint(worldPoint) {
|
|
406
|
+
const ro = this._realObject;
|
|
407
|
+
if (!ro?.object3d)
|
|
408
|
+
return null;
|
|
409
|
+
const local = new THREE.Vector3().copy(worldPoint);
|
|
410
|
+
const mInv = new THREE.Matrix4().copy(ro.object3d.matrixWorld).invert();
|
|
411
|
+
local.applyMatrix4(mInv);
|
|
412
|
+
const cols = this.columns;
|
|
413
|
+
const rows = this.rackRows;
|
|
414
|
+
const shelves = this.shelves;
|
|
415
|
+
const width = this.state.width || 1000;
|
|
416
|
+
const height = this.state.depth || 2000; // 3D Y
|
|
417
|
+
const depth = this.state.height || 200; // 3D Z
|
|
418
|
+
const shelfBase = this.state.shelfBaseHeight || 0;
|
|
419
|
+
const shelfZone = height - shelfBase;
|
|
420
|
+
const bayW = width / cols;
|
|
421
|
+
const bayD = depth / rows;
|
|
422
|
+
const cellY = shelfZone / shelves;
|
|
423
|
+
// center-based: X [-width/2, +width/2], Y [-height/2, +height/2], Z [-depth/2, +depth/2]
|
|
424
|
+
const col = Math.floor((local.x + width / 2) / bayW);
|
|
425
|
+
const yFromBottom = local.y + height / 2 - shelfBase;
|
|
426
|
+
const shelf = Math.floor(yFromBottom / cellY);
|
|
427
|
+
const row = Math.floor((local.z + depth / 2) / bayD);
|
|
428
|
+
if (col < 0 || col >= cols || row < 0 || row >= rows || shelf < 0 || shelf >= shelves) {
|
|
429
|
+
return `out-of-bounds(col=${col},row=${row},shelf=${shelf})`;
|
|
430
|
+
}
|
|
431
|
+
return `${col}-${row}-${shelf}`;
|
|
432
|
+
}
|
|
433
|
+
resolveLegendColor(record) {
|
|
434
|
+
const legend = this.legendTarget;
|
|
435
|
+
if (!legend)
|
|
436
|
+
return undefined;
|
|
437
|
+
const status = legend.getState?.('status') ?? legend.state?.status;
|
|
438
|
+
if (!status)
|
|
439
|
+
return undefined;
|
|
440
|
+
const field = status.field;
|
|
441
|
+
const ranges = status.ranges;
|
|
442
|
+
if (!field || !Array.isArray(ranges))
|
|
443
|
+
return undefined;
|
|
444
|
+
const value = record?.[field];
|
|
445
|
+
if (value === undefined || value === null)
|
|
446
|
+
return status.defaultColor;
|
|
447
|
+
for (const range of ranges) {
|
|
448
|
+
if (!range)
|
|
449
|
+
continue;
|
|
450
|
+
if (range.value !== undefined) {
|
|
451
|
+
if (range.value === value)
|
|
452
|
+
return range.color;
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
const num = Number(value);
|
|
456
|
+
if (!Number.isFinite(num))
|
|
457
|
+
continue;
|
|
458
|
+
const min = range.min !== undefined && range.min !== '' ? Number(range.min) : undefined;
|
|
459
|
+
const max = range.max !== undefined && range.max !== '' ? Number(range.max) : undefined;
|
|
460
|
+
const minOk = min === undefined || num >= min;
|
|
461
|
+
const maxOk = max === undefined || num < max;
|
|
462
|
+
if (minOk && maxOk)
|
|
463
|
+
return range.color;
|
|
464
|
+
}
|
|
465
|
+
return status.defaultColor;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* 새 (rows × columns) 에 맞춰 children 재구성. rack-table.buildCells 정확 클론.
|
|
469
|
+
*/
|
|
470
|
+
_buildCells(newrows, newcolumns, oldrows, oldcolumns) {
|
|
471
|
+
const app = this.app;
|
|
337
472
|
if (newrows < oldrows) {
|
|
338
|
-
|
|
339
|
-
this.remove(removals);
|
|
473
|
+
const removals = this.components.slice(oldcolumns * newrows);
|
|
474
|
+
this.remove?.(removals);
|
|
340
475
|
}
|
|
341
476
|
const minrows = Math.min(newrows, oldrows);
|
|
342
477
|
if (newcolumns > oldcolumns) {
|
|
343
478
|
for (let r = 0; r < minrows; r++) {
|
|
344
479
|
for (let c = oldcolumns; c < newcolumns; c++) {
|
|
345
|
-
|
|
480
|
+
;
|
|
481
|
+
this.insertComponentAt?.(buildNewCell(app), r * newcolumns + c);
|
|
346
482
|
}
|
|
347
483
|
}
|
|
348
484
|
}
|
|
349
485
|
else if (newcolumns < oldcolumns) {
|
|
350
|
-
|
|
486
|
+
const removals = [];
|
|
351
487
|
for (let r = 0; r < minrows; r++) {
|
|
352
488
|
for (let c = newcolumns; c < oldcolumns; c++) {
|
|
353
489
|
removals.push(this.components[r * oldcolumns + c]);
|
|
354
490
|
}
|
|
355
491
|
}
|
|
356
|
-
|
|
492
|
+
;
|
|
493
|
+
this.remove?.(removals);
|
|
357
494
|
}
|
|
358
495
|
if (newrows > oldrows) {
|
|
359
|
-
|
|
496
|
+
const newbies = [];
|
|
360
497
|
for (let r = oldrows; r < newrows; r++) {
|
|
361
|
-
for (let i = 0; i < newcolumns; i++)
|
|
362
|
-
newbies.push(buildNewCell(
|
|
363
|
-
}
|
|
498
|
+
for (let i = 0; i < newcolumns; i++)
|
|
499
|
+
newbies.push(buildNewCell(app));
|
|
364
500
|
}
|
|
365
|
-
|
|
501
|
+
;
|
|
502
|
+
this.add?.(newbies);
|
|
366
503
|
}
|
|
367
|
-
this.set({
|
|
368
|
-
|
|
369
|
-
heights: this.heights
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
get layout() {
|
|
373
|
-
return TABLE_LAYOUT;
|
|
374
|
-
}
|
|
375
|
-
get rows() {
|
|
376
|
-
return this.getState('rows');
|
|
377
|
-
}
|
|
378
|
-
setCellsStyle(cells, style, where) {
|
|
379
|
-
const components = this.components;
|
|
380
|
-
const total = components.length;
|
|
381
|
-
const columns = this.getState('columns');
|
|
382
|
-
// 병합된 셀도 포함시킨다.
|
|
383
|
-
const _cells = [];
|
|
384
|
-
cells.forEach(c => {
|
|
385
|
-
_cells.push(c);
|
|
386
|
-
if (c.colspan || c.rowspan) {
|
|
387
|
-
let col = this.getRowColumn(c).column;
|
|
388
|
-
let row = this.getRowColumn(c).row;
|
|
389
|
-
for (let i = row; i < row + c.rowspan; i++)
|
|
390
|
-
for (let j = col; j < col + c.colspan; j++)
|
|
391
|
-
if (i != row || j != col)
|
|
392
|
-
_cells.push(this.components[i * this.columns + j]);
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
const indices = _cells.map(cell => components.indexOf(cell));
|
|
396
|
-
indices.forEach(i => {
|
|
397
|
-
const cell = components[i];
|
|
398
|
-
switch (where) {
|
|
399
|
-
case 'all':
|
|
400
|
-
setCellBorder(cell, style, where);
|
|
401
|
-
if (isLeftMost(total, columns, indices, i))
|
|
402
|
-
setCellBorder(components[before(columns, i)], style, 'right');
|
|
403
|
-
if (isRightMost(total, columns, indices, i))
|
|
404
|
-
setCellBorder(components[after(columns, i)], style, 'left');
|
|
405
|
-
if (isTopMost(total, columns, indices, i))
|
|
406
|
-
setCellBorder(components[above(columns, i)], style, 'bottom');
|
|
407
|
-
if (isBottomMost(total, columns, indices, i))
|
|
408
|
-
setCellBorder(components[below(columns, i)], style, 'top');
|
|
409
|
-
break;
|
|
410
|
-
case 'in':
|
|
411
|
-
if (!isLeftMost(total, columns, indices, i)) {
|
|
412
|
-
setCellBorder(cell, style, 'left');
|
|
413
|
-
}
|
|
414
|
-
if (!isRightMost(total, columns, indices, i)) {
|
|
415
|
-
setCellBorder(cell, style, 'right');
|
|
416
|
-
}
|
|
417
|
-
if (!isTopMost(total, columns, indices, i)) {
|
|
418
|
-
setCellBorder(cell, style, 'top');
|
|
419
|
-
}
|
|
420
|
-
if (!isBottomMost(total, columns, indices, i)) {
|
|
421
|
-
setCellBorder(cell, style, 'bottom');
|
|
422
|
-
}
|
|
423
|
-
break;
|
|
424
|
-
case 'out':
|
|
425
|
-
if (isLeftMost(total, columns, indices, i)) {
|
|
426
|
-
setCellBorder(cell, style, 'left');
|
|
427
|
-
setCellBorder(components[before(columns, i)], style, 'right');
|
|
428
|
-
}
|
|
429
|
-
if (isRightMost(total, columns, indices, i)) {
|
|
430
|
-
setCellBorder(cell, style, 'right');
|
|
431
|
-
setCellBorder(components[after(columns, i)], style, 'left');
|
|
432
|
-
}
|
|
433
|
-
if (isTopMost(total, columns, indices, i)) {
|
|
434
|
-
setCellBorder(cell, style, 'top');
|
|
435
|
-
setCellBorder(components[above(columns, i)], style, 'bottom');
|
|
436
|
-
}
|
|
437
|
-
if (isBottomMost(total, columns, indices, i)) {
|
|
438
|
-
setCellBorder(cell, style, 'bottom');
|
|
439
|
-
setCellBorder(components[below(columns, i)], style, 'top');
|
|
440
|
-
}
|
|
441
|
-
break;
|
|
442
|
-
case 'left':
|
|
443
|
-
if (isLeftMost(total, columns, indices, i)) {
|
|
444
|
-
setCellBorder(cell, style, 'left');
|
|
445
|
-
setCellBorder(components[before(columns, i)], style, 'right');
|
|
446
|
-
}
|
|
447
|
-
break;
|
|
448
|
-
case 'right':
|
|
449
|
-
if (isRightMost(total, columns, indices, i)) {
|
|
450
|
-
setCellBorder(cell, style, 'right');
|
|
451
|
-
setCellBorder(components[after(columns, i)], style, 'left');
|
|
452
|
-
}
|
|
453
|
-
break;
|
|
454
|
-
case 'center':
|
|
455
|
-
if (!isLeftMost(total, columns, indices, i)) {
|
|
456
|
-
setCellBorder(cell, style, 'left');
|
|
457
|
-
}
|
|
458
|
-
if (!isRightMost(total, columns, indices, i)) {
|
|
459
|
-
setCellBorder(cell, style, 'right');
|
|
460
|
-
}
|
|
461
|
-
break;
|
|
462
|
-
case 'middle':
|
|
463
|
-
if (!isTopMost(total, columns, indices, i)) {
|
|
464
|
-
setCellBorder(cell, style, 'top');
|
|
465
|
-
}
|
|
466
|
-
if (!isBottomMost(total, columns, indices, i)) {
|
|
467
|
-
setCellBorder(cell, style, 'bottom');
|
|
468
|
-
}
|
|
469
|
-
break;
|
|
470
|
-
case 'top':
|
|
471
|
-
if (isTopMost(total, columns, indices, i)) {
|
|
472
|
-
setCellBorder(cell, style, 'top');
|
|
473
|
-
setCellBorder(components[above(columns, i)], style, 'bottom');
|
|
474
|
-
}
|
|
475
|
-
break;
|
|
476
|
-
case 'bottom':
|
|
477
|
-
if (isBottomMost(total, columns, indices, i)) {
|
|
478
|
-
setCellBorder(cell, style, 'bottom');
|
|
479
|
-
setCellBorder(components[below(columns, i)], style, 'top');
|
|
480
|
-
}
|
|
481
|
-
break;
|
|
482
|
-
case 'clear':
|
|
483
|
-
setCellBorder(cell, CLEAR_STYLE, 'all');
|
|
484
|
-
if (isLeftMost(total, columns, indices, i))
|
|
485
|
-
setCellBorder(components[before(columns, i)], CLEAR_STYLE, 'right');
|
|
486
|
-
if (isRightMost(total, columns, indices, i))
|
|
487
|
-
setCellBorder(components[after(columns, i)], CLEAR_STYLE, 'left');
|
|
488
|
-
if (isTopMost(total, columns, indices, i))
|
|
489
|
-
setCellBorder(components[above(columns, i)], CLEAR_STYLE, 'bottom');
|
|
490
|
-
if (isBottomMost(total, columns, indices, i))
|
|
491
|
-
setCellBorder(components[below(columns, i)], CLEAR_STYLE, 'top');
|
|
492
|
-
}
|
|
493
|
-
});
|
|
504
|
+
this.set({ widths: this.widths, heights: this.heights });
|
|
505
|
+
this._syncChildCellIds();
|
|
494
506
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
507
|
+
/** children 의 state.cellId 가 *순서 기반 bayKey* 와 일치하도록 동기화. */
|
|
508
|
+
_syncChildCellIds() {
|
|
509
|
+
const cols = this.columns;
|
|
510
|
+
const children = this.components;
|
|
511
|
+
for (let i = 0; i < children.length; i++) {
|
|
512
|
+
const col = i % cols;
|
|
513
|
+
const row = Math.floor(i / cols);
|
|
514
|
+
const bayKey = `${col}-${row}`;
|
|
515
|
+
const c = children[i];
|
|
516
|
+
if (c?.state?.cellId !== bayKey)
|
|
517
|
+
c?.set?.('cellId', bayKey);
|
|
518
|
+
}
|
|
501
519
|
}
|
|
520
|
+
/** Row 인덱스의 cell 들. rack-table-cell 의 rowCells 가 사용. */
|
|
502
521
|
getCellsByRow(row) {
|
|
503
522
|
return this.components.slice(row * this.columns, (row + 1) * this.columns);
|
|
504
523
|
}
|
|
524
|
+
/** Column 인덱스의 cell 들. */
|
|
505
525
|
getCellsByColumn(column) {
|
|
506
|
-
const
|
|
507
|
-
for (let i = 0; i < this.
|
|
508
|
-
|
|
509
|
-
return
|
|
510
|
-
}
|
|
511
|
-
deleteRows(cells) {
|
|
512
|
-
// 먼저 cells 위치의 행을 구한다.
|
|
513
|
-
let rows = [];
|
|
514
|
-
cells.forEach(cell => {
|
|
515
|
-
let row = this.getRowColumn(cell).row;
|
|
516
|
-
if (-1 == rows.indexOf(row)) {
|
|
517
|
-
rows.push(row);
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
rows.sort((a, b) => {
|
|
521
|
-
return a - b;
|
|
522
|
-
});
|
|
523
|
-
rows.reverse();
|
|
524
|
-
const heights = this.heights.slice();
|
|
525
|
-
rows.forEach(row => {
|
|
526
|
-
this.remove(this.getCellsByRow(row));
|
|
527
|
-
});
|
|
528
|
-
rows.forEach(row => heights.splice(row, 1));
|
|
529
|
-
this.model.rows -= rows.length; // 고의적으로, change 이벤트가 발생하지 않도록 set(..)을 사용하지 않음.
|
|
530
|
-
this.set('heights', heights);
|
|
531
|
-
}
|
|
532
|
-
deleteColumns(cells) {
|
|
533
|
-
// 먼저 cells 위치의 열을 구한다.
|
|
534
|
-
let columns = [];
|
|
535
|
-
cells.forEach(cell => {
|
|
536
|
-
let column = this.getRowColumn(cell).column;
|
|
537
|
-
if (-1 == columns.indexOf(column))
|
|
538
|
-
columns.push(column);
|
|
539
|
-
});
|
|
540
|
-
columns.sort((a, b) => {
|
|
541
|
-
return a - b;
|
|
542
|
-
});
|
|
543
|
-
columns.reverse();
|
|
544
|
-
columns.forEach(column => {
|
|
545
|
-
const widths = this.widths.slice();
|
|
546
|
-
this.remove(this.getCellsByColumn(column));
|
|
547
|
-
widths.splice(column, 1);
|
|
548
|
-
this.model.columns -= 1; // 고의적으로, change 이벤트가 발생하지 않도록 set(..)을 사용하지 않음.
|
|
549
|
-
this.set('widths', widths);
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
insertCellsAbove(cells) {
|
|
553
|
-
// 먼저 cells 위치의 행을 구한다.
|
|
554
|
-
let rows = [];
|
|
555
|
-
cells.forEach(cell => {
|
|
556
|
-
let row = this.getRowColumn(cell).row;
|
|
557
|
-
if (-1 == rows.indexOf(row))
|
|
558
|
-
rows.push(row);
|
|
559
|
-
});
|
|
560
|
-
rows.sort((a, b) => {
|
|
561
|
-
return a - b;
|
|
562
|
-
});
|
|
563
|
-
rows.reverse();
|
|
564
|
-
// 행 2개 이상은 추가 안함. 임시로 막아놓음
|
|
565
|
-
if (rows.length >= 2)
|
|
566
|
-
return false;
|
|
567
|
-
let insertionRowPosition = rows[0];
|
|
568
|
-
let newbieRowHeights = [];
|
|
569
|
-
let newbieCells = [];
|
|
570
|
-
rows.forEach(row => {
|
|
571
|
-
for (let i = 0; i < this.columns; i++)
|
|
572
|
-
newbieCells.push(buildCopiedCell(this.components[row * this.columns + i].model, this.app));
|
|
573
|
-
newbieRowHeights.push(this.heights[row]);
|
|
574
|
-
newbieCells.reverse().forEach(cell => {
|
|
575
|
-
this.insertComponentAt(cell, insertionRowPosition * this.columns);
|
|
576
|
-
});
|
|
577
|
-
let heights = this.heights.slice();
|
|
578
|
-
heights.splice(insertionRowPosition, 0, ...newbieRowHeights);
|
|
579
|
-
this.set('heights', heights);
|
|
580
|
-
this.model.rows += rows.length;
|
|
581
|
-
this.clearCache();
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
insertCellsBelow(cells) {
|
|
585
|
-
// 먼저 cells 위치의 행을 구한다.
|
|
586
|
-
let rows = [];
|
|
587
|
-
cells.forEach(cell => {
|
|
588
|
-
let row = this.getRowColumn(cell).row;
|
|
589
|
-
if (-1 == rows.indexOf(row))
|
|
590
|
-
rows.push(row);
|
|
591
|
-
});
|
|
592
|
-
rows.sort((a, b) => {
|
|
593
|
-
return a - b;
|
|
594
|
-
});
|
|
595
|
-
rows.reverse();
|
|
596
|
-
// 행 2개 이상은 추가 안함. 임시로 막아놓음
|
|
597
|
-
if (rows.length >= 2)
|
|
598
|
-
return false;
|
|
599
|
-
let insertionRowPosition = rows[rows.length - 1] + 1;
|
|
600
|
-
let newbieRowHeights = [];
|
|
601
|
-
let newbieCells = [];
|
|
602
|
-
rows.forEach(row => {
|
|
603
|
-
for (let i = 0; i < this.columns; i++)
|
|
604
|
-
newbieCells.push(buildCopiedCell(this.components[row * this.columns + i].model, this.app));
|
|
605
|
-
newbieRowHeights.push(this.heights[row]);
|
|
606
|
-
newbieCells.reverse().forEach(cell => {
|
|
607
|
-
this.insertComponentAt(cell, insertionRowPosition * this.columns);
|
|
608
|
-
});
|
|
609
|
-
let heights = this.heights.slice();
|
|
610
|
-
heights.splice(insertionRowPosition, 0, ...newbieRowHeights);
|
|
611
|
-
this.set('heights', heights);
|
|
612
|
-
this.model.rows += 1;
|
|
613
|
-
this.clearCache();
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
insertCellsLeft(cells) {
|
|
617
|
-
// 먼저 cells 위치의 열을 구한다.
|
|
618
|
-
let columns = [];
|
|
619
|
-
cells.forEach(cell => {
|
|
620
|
-
let column = this.getRowColumn(cell).column;
|
|
621
|
-
if (-1 == columns.indexOf(column))
|
|
622
|
-
columns.push(column);
|
|
623
|
-
});
|
|
624
|
-
columns.sort((a, b) => {
|
|
625
|
-
return a - b;
|
|
626
|
-
});
|
|
627
|
-
columns.reverse();
|
|
628
|
-
// 열 2개 이상은 추가 안함. 임시로 막아놓음
|
|
629
|
-
if (columns.length >= 2)
|
|
630
|
-
return false;
|
|
631
|
-
let insertionColumnPosition = columns[0];
|
|
632
|
-
let newbieColumnWidths = [];
|
|
633
|
-
let newbieCells = [];
|
|
634
|
-
columns.forEach(column => {
|
|
635
|
-
for (let i = 0; i < this.rows; i++)
|
|
636
|
-
newbieCells.push(buildCopiedCell(this.components[column + this.columns * i].model, this.app));
|
|
637
|
-
newbieColumnWidths.push(this.widths[column]);
|
|
638
|
-
let increasedColumns = this.columns;
|
|
639
|
-
let index = this.rows;
|
|
640
|
-
newbieCells.reverse().forEach(cell => {
|
|
641
|
-
if (index == 0) {
|
|
642
|
-
index = this.rows;
|
|
643
|
-
increasedColumns++;
|
|
644
|
-
}
|
|
645
|
-
index--;
|
|
646
|
-
this.insertComponentAt(cell, insertionColumnPosition + index * increasedColumns);
|
|
647
|
-
});
|
|
648
|
-
let widths = this.widths.slice();
|
|
649
|
-
this.model.columns += columns.length; // 고의적으로, change 이벤트가 발생하지 않도록 set(..)을 사용하지 않음.
|
|
650
|
-
widths.splice(insertionColumnPosition, 0, ...newbieColumnWidths);
|
|
651
|
-
this.set('widths', widths);
|
|
652
|
-
});
|
|
653
|
-
}
|
|
654
|
-
insertCellsRight(cells) {
|
|
655
|
-
// 먼저 cells 위치의 열을 구한다.
|
|
656
|
-
let columns = [];
|
|
657
|
-
cells.forEach(cell => {
|
|
658
|
-
let column = this.getRowColumn(cell).column;
|
|
659
|
-
if (-1 == columns.indexOf(column))
|
|
660
|
-
columns.push(column);
|
|
661
|
-
});
|
|
662
|
-
columns.sort((a, b) => {
|
|
663
|
-
return a - b;
|
|
664
|
-
});
|
|
665
|
-
columns.reverse();
|
|
666
|
-
// 열 2개 이상은 추가 안함. 임시로 막아놓음
|
|
667
|
-
if (columns.length >= 2)
|
|
668
|
-
return false;
|
|
669
|
-
let insertionColumnPosition = columns[columns.length - 1] + 1;
|
|
670
|
-
let newbieColumnWidths = [];
|
|
671
|
-
let newbieCells = [];
|
|
672
|
-
columns.forEach(column => {
|
|
673
|
-
for (let i = 0; i < this.rows; i++)
|
|
674
|
-
newbieCells.push(buildCopiedCell(this.components[column + this.columns * i].model, this.app));
|
|
675
|
-
newbieColumnWidths.push(this.widths[column]);
|
|
676
|
-
let increasedColumns = this.columns;
|
|
677
|
-
let index = this.rows;
|
|
678
|
-
newbieCells.reverse().forEach(cell => {
|
|
679
|
-
if (index == 0) {
|
|
680
|
-
index = this.rows;
|
|
681
|
-
increasedColumns++;
|
|
682
|
-
}
|
|
683
|
-
index--;
|
|
684
|
-
this.insertComponentAt(cell, insertionColumnPosition + index * increasedColumns);
|
|
685
|
-
});
|
|
686
|
-
let widths = this.widths.slice();
|
|
687
|
-
this.model.columns += columns.length; // 고의적으로, change 이벤트가 발생하지 않도록 set(..)을 사용하지 않음.
|
|
688
|
-
widths.splice(insertionColumnPosition, 0, ...newbieColumnWidths);
|
|
689
|
-
this.set('widths', widths);
|
|
690
|
-
});
|
|
691
|
-
}
|
|
692
|
-
distributeHorizontal(cells) {
|
|
693
|
-
const columns = [];
|
|
694
|
-
cells.forEach(cell => {
|
|
695
|
-
let rowcolumn = this.getRowColumn(cell);
|
|
696
|
-
if (-1 == columns.indexOf(rowcolumn.column))
|
|
697
|
-
columns.push(rowcolumn.column);
|
|
698
|
-
});
|
|
699
|
-
const sum = columns.reduce((sum, column) => {
|
|
700
|
-
return sum + this.widths[column];
|
|
701
|
-
}, 0);
|
|
702
|
-
const newval = Math.round((sum / columns.length) * 100) / 100;
|
|
703
|
-
const widths = this.widths.slice();
|
|
704
|
-
columns.forEach(column => {
|
|
705
|
-
widths[column] = newval;
|
|
706
|
-
});
|
|
707
|
-
this.set('widths', widths);
|
|
708
|
-
}
|
|
709
|
-
distributeVertical(cells) {
|
|
710
|
-
const rows = [];
|
|
711
|
-
cells.forEach(cell => {
|
|
712
|
-
let rowcolumn = this.getRowColumn(cell);
|
|
713
|
-
if (-1 == rows.indexOf(rowcolumn.row))
|
|
714
|
-
rows.push(rowcolumn.row);
|
|
715
|
-
});
|
|
716
|
-
const sum = rows.reduce((sum, row) => {
|
|
717
|
-
return sum + this.heights[row];
|
|
718
|
-
}, 0);
|
|
719
|
-
const newval = Math.round((sum / rows.length) * 100) / 100;
|
|
720
|
-
const heights = this.heights.slice();
|
|
721
|
-
rows.forEach(row => {
|
|
722
|
-
heights[row] = newval;
|
|
723
|
-
});
|
|
724
|
-
this.set('heights', heights);
|
|
725
|
-
}
|
|
726
|
-
increaseLocation(type, skipNumbering, startSection, startUnit) {
|
|
727
|
-
const { sectionDigits = 2, unitDigits = 2 } = this.state;
|
|
728
|
-
const selectedCells = this.root.selected;
|
|
729
|
-
increaseLocation(selectedCells, type, skipNumbering, startSection, startUnit, sectionDigits, unitDigits);
|
|
730
|
-
}
|
|
731
|
-
get columns() {
|
|
732
|
-
return this.getState('columns');
|
|
526
|
+
const out = [];
|
|
527
|
+
for (let i = 0; i < this.rackRows; i++)
|
|
528
|
+
out.push(this.components[this.columns * i + column]);
|
|
529
|
+
return out;
|
|
733
530
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
531
|
+
/** (col, row) 위치의 cell-component. */
|
|
532
|
+
cellAt(col, row = 0) {
|
|
533
|
+
const idx = row * this.columns + col;
|
|
534
|
+
return this.components[idx] ?? null;
|
|
738
535
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
536
|
+
/** bayKey 의 cell-component. */
|
|
537
|
+
cellAtBayKey(bayKey) {
|
|
538
|
+
const parsed = this.parsePosKey(bayKey);
|
|
539
|
+
if (!parsed)
|
|
540
|
+
return null;
|
|
541
|
+
return this.cellAt(parsed.col, parsed.row);
|
|
743
542
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
543
|
+
/**
|
|
544
|
+
* (col, row) bay 가 *isEmpty* 인가. cell-component 의 state.isEmpty 가 source of
|
|
545
|
+
* truth, 없으면 cellOverrides[posKey].isEmpty fallback.
|
|
546
|
+
*/
|
|
547
|
+
isBayEmpty(col, row = 0) {
|
|
548
|
+
const cell = this.cellAt(col, row);
|
|
549
|
+
if (cell)
|
|
550
|
+
return !!cell.state.isEmpty;
|
|
551
|
+
const posKey = `${col}-${row}`;
|
|
552
|
+
return !!this.cellOverrides[posKey]?.isEmpty;
|
|
748
553
|
}
|
|
749
|
-
|
|
750
|
-
return
|
|
554
|
+
buildRealObject() {
|
|
555
|
+
return new RackGrid3D(this);
|
|
751
556
|
}
|
|
752
|
-
|
|
753
|
-
|
|
557
|
+
// ── Table layout — 자식 자동 배치 + columns/rows 의 가변 widths/heights ──
|
|
558
|
+
get layout() {
|
|
559
|
+
return TABLE_LAYOUT;
|
|
754
560
|
}
|
|
755
|
-
get
|
|
756
|
-
|
|
561
|
+
get widths() {
|
|
562
|
+
const widths = this.getState('widths');
|
|
563
|
+
if (!widths)
|
|
564
|
+
return arrayOf(1, this.columns);
|
|
565
|
+
if (widths.length < this.columns)
|
|
566
|
+
return widths.concat(arrayOf(1, this.columns - widths.length));
|
|
567
|
+
if (widths.length > this.columns)
|
|
568
|
+
return widths.slice(0, this.columns);
|
|
569
|
+
return widths;
|
|
757
570
|
}
|
|
758
|
-
get
|
|
759
|
-
|
|
571
|
+
get heights() {
|
|
572
|
+
const heights = this.getState('heights');
|
|
573
|
+
if (!heights)
|
|
574
|
+
return arrayOf(1, this.rackRows);
|
|
575
|
+
if (heights.length < this.rackRows)
|
|
576
|
+
return heights.concat(arrayOf(1, this.rackRows - heights.length));
|
|
577
|
+
if (heights.length > this.rackRows)
|
|
578
|
+
return heights.slice(0, this.rackRows);
|
|
579
|
+
return heights;
|
|
760
580
|
}
|
|
761
581
|
get widths_sum() {
|
|
762
|
-
|
|
763
|
-
return widths ? widths.filter((width, i) => i < this.columns).reduce((sum, width) => sum + width, 0) : this.columns;
|
|
582
|
+
return this.widths.reduce((sum, w) => sum + w, 0) || this.columns;
|
|
764
583
|
}
|
|
765
584
|
get heights_sum() {
|
|
766
|
-
|
|
767
|
-
return heights ? heights.filter((height, i) => i < this.rows).reduce((sum, height) => sum + height, 0) : this.rows;
|
|
768
|
-
}
|
|
769
|
-
get nature() {
|
|
770
|
-
return NATURE;
|
|
585
|
+
return this.heights.reduce((sum, h) => sum + h, 0) || this.rackRows;
|
|
771
586
|
}
|
|
587
|
+
/** Column / Row 사이 드래그 핸들 — editor 의 widths/heights 가변 조절. */
|
|
772
588
|
get controls() {
|
|
773
589
|
const widths = this.widths;
|
|
774
590
|
const heights = this.heights;
|
|
775
591
|
const inside = this.textBounds;
|
|
776
592
|
const width_unit = inside.width / this.widths_sum;
|
|
777
593
|
const height_unit = inside.height / this.heights_sum;
|
|
778
|
-
let x = inside.left;
|
|
779
|
-
let y = inside.top;
|
|
780
594
|
const controls = [];
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
y: inside.top,
|
|
786
|
-
handler: columnControlHandler
|
|
787
|
-
});
|
|
595
|
+
let x = inside.left;
|
|
596
|
+
widths.slice(0, this.columns - 1).forEach(w => {
|
|
597
|
+
x += w * width_unit;
|
|
598
|
+
controls.push({ x, y: inside.top, handler: columnControlHandler });
|
|
788
599
|
});
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
y: y,
|
|
794
|
-
handler: rowControlHandler
|
|
795
|
-
});
|
|
600
|
+
let y = inside.top;
|
|
601
|
+
heights.slice(0, this.rackRows - 1).forEach(h => {
|
|
602
|
+
y += h * height_unit;
|
|
603
|
+
controls.push({ x: inside.left, y, handler: rowControlHandler });
|
|
796
604
|
});
|
|
797
605
|
return controls;
|
|
798
606
|
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
607
|
+
// ── 2D 렌더링 — table layout 의 widths/heights 기반 격자 ─────
|
|
608
|
+
//
|
|
609
|
+
// 자식 컴포넌트 가 자기 렌더링하지만 *비어있는 grid 도 modeling 시 가시화*. 격자
|
|
610
|
+
// 좌표는 widths/heights (table layout 의 가변 분할) 기반. cellOverrides 의
|
|
611
|
+
// isEmpty (사선) + border (4-side) 도 표시.
|
|
612
|
+
render(ctx) {
|
|
613
|
+
// 2D 에서는 hideRackFrame 이어도 *외곽 + 격자선 + cell 마커* 는 항상 그림.
|
|
614
|
+
// (2D 의 frame 은 시각적 *분할선* 이라 frame mesh 와 의미가 다름)
|
|
615
|
+
const inside = this.textBounds;
|
|
616
|
+
const left = inside.left;
|
|
617
|
+
const top = inside.top;
|
|
618
|
+
const width = inside.width;
|
|
619
|
+
const height = inside.height;
|
|
620
|
+
const cols = this.columns;
|
|
621
|
+
const rows = this.rackRows;
|
|
622
|
+
const widths = this.widths;
|
|
623
|
+
const heights = this.heights;
|
|
624
|
+
const wSum = this.widths_sum;
|
|
625
|
+
const hSum = this.heights_sum;
|
|
626
|
+
const fill = this.state.fillStyle || '#e8e8ec';
|
|
627
|
+
const stroke = this.state.strokeStyle || '#888';
|
|
628
|
+
const lineWidth = this.state.lineWidth || 1;
|
|
629
|
+
const overrides = this.cellOverrides;
|
|
630
|
+
// column 별 X 좌표 (좌→우 누적)
|
|
631
|
+
const xs = [left];
|
|
632
|
+
for (let c = 0; c < cols; c++)
|
|
633
|
+
xs.push(xs[c] + (widths[c] / wSum) * width);
|
|
634
|
+
// row 별 Y 좌표 (상→하 누적)
|
|
635
|
+
const ys = [top];
|
|
636
|
+
for (let r = 0; r < rows; r++)
|
|
637
|
+
ys.push(ys[r] + (heights[r] / hSum) * height);
|
|
638
|
+
// Fill (반투명) — 외곽 영역
|
|
639
|
+
ctx.save();
|
|
640
|
+
ctx.fillStyle = fill;
|
|
641
|
+
ctx.globalAlpha = 0.2;
|
|
642
|
+
ctx.fillRect(left, top, width, height);
|
|
643
|
+
ctx.restore();
|
|
644
|
+
// isEmpty cell 표시 (사선 + 회색 fill)
|
|
645
|
+
ctx.save();
|
|
646
|
+
for (const [posKey, ov] of Object.entries(overrides)) {
|
|
647
|
+
if (!ov.isEmpty)
|
|
648
|
+
continue;
|
|
649
|
+
const parsed = this.parsePosKey(posKey);
|
|
650
|
+
if (!parsed)
|
|
651
|
+
continue;
|
|
652
|
+
if (parsed.col < 0 || parsed.col >= cols || parsed.row < 0 || parsed.row >= rows)
|
|
653
|
+
continue;
|
|
654
|
+
const cl = xs[parsed.col];
|
|
655
|
+
const ct = ys[parsed.row];
|
|
656
|
+
const cw = xs[parsed.col + 1] - cl;
|
|
657
|
+
const ch = ys[parsed.row + 1] - ct;
|
|
658
|
+
ctx.fillStyle = '#cccccc';
|
|
659
|
+
ctx.globalAlpha = 0.5;
|
|
660
|
+
ctx.fillRect(cl, ct, cw, ch);
|
|
661
|
+
ctx.globalAlpha = 1;
|
|
662
|
+
ctx.strokeStyle = '#999';
|
|
663
|
+
ctx.lineWidth = 1;
|
|
664
|
+
ctx.beginPath();
|
|
665
|
+
ctx.moveTo(cl, ct);
|
|
666
|
+
ctx.lineTo(cl + cw, ct + ch);
|
|
667
|
+
ctx.moveTo(cl + cw, ct);
|
|
668
|
+
ctx.lineTo(cl, ct + ch);
|
|
669
|
+
ctx.stroke();
|
|
670
|
+
}
|
|
671
|
+
ctx.restore();
|
|
672
|
+
// 외곽 + 격자
|
|
673
|
+
ctx.save();
|
|
674
|
+
ctx.strokeStyle = stroke;
|
|
675
|
+
ctx.lineWidth = lineWidth;
|
|
676
|
+
ctx.strokeRect(left, top, width, height);
|
|
677
|
+
ctx.beginPath();
|
|
678
|
+
for (let c = 1; c < cols; c++) {
|
|
679
|
+
ctx.moveTo(xs[c], top);
|
|
680
|
+
ctx.lineTo(xs[c], top + height);
|
|
804
681
|
}
|
|
805
|
-
|
|
806
|
-
|
|
682
|
+
for (let r = 1; r < rows; r++) {
|
|
683
|
+
ctx.moveTo(left, ys[r]);
|
|
684
|
+
ctx.lineTo(left + width, ys[r]);
|
|
807
685
|
}
|
|
808
|
-
|
|
686
|
+
ctx.stroke();
|
|
687
|
+
ctx.restore();
|
|
688
|
+
// cell 별 명시 border
|
|
689
|
+
for (const [posKey, ov] of Object.entries(overrides)) {
|
|
690
|
+
if (!ov.border)
|
|
691
|
+
continue;
|
|
692
|
+
const parsed = this.parsePosKey(posKey);
|
|
693
|
+
if (!parsed)
|
|
694
|
+
continue;
|
|
695
|
+
if (parsed.col < 0 || parsed.col >= cols || parsed.row < 0 || parsed.row >= rows)
|
|
696
|
+
continue;
|
|
697
|
+
const cl = xs[parsed.col];
|
|
698
|
+
const ct = ys[parsed.row];
|
|
699
|
+
const cr = xs[parsed.col + 1];
|
|
700
|
+
const cb = ys[parsed.row + 1];
|
|
701
|
+
const b = ov.border;
|
|
702
|
+
const drawSide = (style, x1, y1, x2, y2) => {
|
|
703
|
+
if (!style?.strokeStyle)
|
|
704
|
+
return;
|
|
705
|
+
ctx.save();
|
|
706
|
+
ctx.strokeStyle = style.strokeStyle;
|
|
707
|
+
ctx.lineWidth = style.lineWidth ?? 1;
|
|
708
|
+
ctx.beginPath();
|
|
709
|
+
ctx.moveTo(x1, y1);
|
|
710
|
+
ctx.lineTo(x2, y2);
|
|
711
|
+
ctx.stroke();
|
|
712
|
+
ctx.restore();
|
|
713
|
+
};
|
|
714
|
+
drawSide(b.top, cl, ct, cr, ct);
|
|
715
|
+
drawSide(b.left, cl, ct, cl, cb);
|
|
716
|
+
drawSide(b.bottom, cl, cb, cr, cb);
|
|
717
|
+
drawSide(b.right, cr, ct, cr, cb);
|
|
718
|
+
}
|
|
719
|
+
// Selected bay 강조는 *rack-grid-cell 자체* 의 render 가 처리 (framework 의
|
|
720
|
+
// selection 시스템이 cell 의 outline 자동 그림). RackGrid 는 grid 골격만.
|
|
809
721
|
}
|
|
810
|
-
|
|
722
|
+
// ── Grid layout ─────────────────────────────────────────
|
|
723
|
+
get columns() {
|
|
724
|
+
return Math.max(1, Math.floor(this.state.columns ?? 5));
|
|
725
|
+
}
|
|
726
|
+
get rackRows() {
|
|
727
|
+
return Math.max(1, Math.floor(this.state.rows ?? 1));
|
|
728
|
+
}
|
|
729
|
+
get shelves() {
|
|
730
|
+
return Math.max(1, Math.floor(this.state.shelves ?? 4));
|
|
731
|
+
}
|
|
732
|
+
// ── cellId / posKey 변환 ────────────────────────────────
|
|
733
|
+
/** posKey = `${col-1}-${row-1}` (0-based, 2 segments). 1-based input. */
|
|
734
|
+
posKeyOf(col, row = 1) {
|
|
735
|
+
return `${col - 1}-${row - 1}`;
|
|
736
|
+
}
|
|
737
|
+
/** cellId = `${col-1}-${row-1}-${shelf-1}` (0-based, 3 segments). 1-based input. */
|
|
738
|
+
cellIdOf(col, row = 1, shelf = 1) {
|
|
739
|
+
return `${col - 1}-${row - 1}-${shelf - 1}`;
|
|
740
|
+
}
|
|
741
|
+
parseCellId(cellId) {
|
|
742
|
+
const parts = cellId.split('-');
|
|
743
|
+
if (parts.length !== 3)
|
|
744
|
+
return null;
|
|
745
|
+
const [c, r, s] = parts.map(Number);
|
|
746
|
+
if (!Number.isFinite(c) || !Number.isFinite(r) || !Number.isFinite(s))
|
|
747
|
+
return null;
|
|
748
|
+
return { col: c, row: r, shelf: s };
|
|
749
|
+
}
|
|
750
|
+
parsePosKey(posKey) {
|
|
751
|
+
const parts = posKey.split('-');
|
|
752
|
+
if (parts.length !== 2)
|
|
753
|
+
return null;
|
|
754
|
+
const [c, r] = parts.map(Number);
|
|
755
|
+
if (!Number.isFinite(c) || !Number.isFinite(r))
|
|
756
|
+
return null;
|
|
757
|
+
return { col: c, row: r };
|
|
758
|
+
}
|
|
759
|
+
// ── cellOverrides accessor ─────────────────────────────
|
|
760
|
+
get cellOverrides() {
|
|
761
|
+
return (this.state.cellOverrides ?? {});
|
|
762
|
+
}
|
|
763
|
+
getCellOverride(posKey) {
|
|
764
|
+
return this.cellOverrides[posKey];
|
|
765
|
+
}
|
|
766
|
+
// ── Shelf labels ───────────────────────────────────────
|
|
767
|
+
/**
|
|
768
|
+
* Parse `state.shelfLocations` 를 levels 길이 array 로. 빈 슬롯은 1-based index default.
|
|
769
|
+
* '1,2,3' → ['1', '2', '3', ...]
|
|
770
|
+
* ',,,04' → ['1', '2', '3', '04']
|
|
771
|
+
* undefined → ['1', '2', '3', '4', ...]
|
|
772
|
+
*/
|
|
773
|
+
get shelfLabels() {
|
|
774
|
+
const input = this.state.shelfLocations;
|
|
775
|
+
const levels = this.shelves;
|
|
776
|
+
const parts = (input || '').split(',');
|
|
777
|
+
const out = [];
|
|
778
|
+
for (let i = 0; i < levels; i++) {
|
|
779
|
+
const p = parts[i];
|
|
780
|
+
out[i] = p && p.trim().length > 0 ? p.trim() : String(i + 1);
|
|
781
|
+
}
|
|
782
|
+
return out;
|
|
783
|
+
}
|
|
784
|
+
// ── Location 변환 (양방향) ──────────────────────────────
|
|
785
|
+
/**
|
|
786
|
+
* cellId → location string. 우선순위:
|
|
787
|
+
* 1. cell-component (state.section/unit) — source of truth
|
|
788
|
+
* 2. cellOverrides[posKey] — 호환성 fallback
|
|
789
|
+
* section/unit 둘 다 있고 isEmpty=false 일 때만 부여. 미부여 시 null.
|
|
790
|
+
*/
|
|
791
|
+
locationOf(cellId) {
|
|
792
|
+
const parsed = this.parseCellId(cellId);
|
|
793
|
+
if (!parsed)
|
|
794
|
+
return null;
|
|
795
|
+
const posKey = `${parsed.col}-${parsed.row}`;
|
|
796
|
+
let section;
|
|
797
|
+
let unit;
|
|
798
|
+
let isEmpty;
|
|
799
|
+
let cellShelfLocations;
|
|
800
|
+
const cell = this.cellAt(parsed.col, parsed.row);
|
|
801
|
+
if (cell) {
|
|
802
|
+
section = cell.state.section;
|
|
803
|
+
unit = cell.state.unit;
|
|
804
|
+
isEmpty = cell.state.isEmpty;
|
|
805
|
+
cellShelfLocations = cell.state.shelfLocations;
|
|
806
|
+
}
|
|
807
|
+
else {
|
|
808
|
+
const override = this.cellOverrides[posKey];
|
|
809
|
+
section = override?.section;
|
|
810
|
+
unit = override?.unit;
|
|
811
|
+
isEmpty = override?.isEmpty;
|
|
812
|
+
}
|
|
813
|
+
if (!section || !unit || isEmpty)
|
|
814
|
+
return null;
|
|
815
|
+
const pattern = this.state.locPattern ?? '{z}{s}-{u}-{sh}';
|
|
816
|
+
const zone = this.state.zone ?? '';
|
|
817
|
+
const shelfLabel = this._shelfLabelsFromCell(cellShelfLocations)[parsed.shelf] ?? String(parsed.shelf + 1);
|
|
818
|
+
return pattern
|
|
819
|
+
.replace(/\{z\}/g, zone)
|
|
820
|
+
.replace(/\{s\}/g, section)
|
|
821
|
+
.replace(/\{u\}/g, unit)
|
|
822
|
+
.replace(/\{sh\}/g, shelfLabel);
|
|
823
|
+
}
|
|
824
|
+
/** cell.state.shelfLocations 명시 시 그것, 아니면 RackGrid 의 shelfLabels. */
|
|
825
|
+
_shelfLabelsFromCell(cellShelfLocations) {
|
|
826
|
+
const input = cellShelfLocations ?? this.state.shelfLocations;
|
|
827
|
+
const levels = this.shelves;
|
|
828
|
+
const parts = (input || '').split(',');
|
|
829
|
+
const out = [];
|
|
830
|
+
for (let i = 0; i < levels; i++) {
|
|
831
|
+
const p = parts[i];
|
|
832
|
+
out[i] = p && p.trim().length > 0 ? p.trim() : String(i + 1);
|
|
833
|
+
}
|
|
834
|
+
return out;
|
|
835
|
+
}
|
|
836
|
+
/** location string → cellId. sparse 역인덱스 lookup. 없으면 null. */
|
|
837
|
+
cellIdOfLocation(location) {
|
|
838
|
+
return this._locationIndex.get(location) ?? null;
|
|
839
|
+
}
|
|
840
|
+
/** 모든 (override 명시된) cell 의 location 목록. inspection 용. */
|
|
841
|
+
get allLocations() {
|
|
842
|
+
const result = [];
|
|
843
|
+
for (const [location, cellId] of this._locationIndex.entries()) {
|
|
844
|
+
result.push({ cellId, location });
|
|
845
|
+
}
|
|
846
|
+
return result;
|
|
847
|
+
}
|
|
848
|
+
// ── Location 역인덱스 (sparse cache) ────────────────────
|
|
849
|
+
_locationIndexCache;
|
|
850
|
+
get _locationIndex() {
|
|
851
|
+
if (this._locationIndexCache)
|
|
852
|
+
return this._locationIndexCache;
|
|
853
|
+
const map = new Map();
|
|
854
|
+
const shelves = this.shelves;
|
|
855
|
+
// 1. cell-component 들 (source of truth) — 모든 bay 순회
|
|
856
|
+
const cells = this.components;
|
|
857
|
+
if (cells && cells.length > 0) {
|
|
858
|
+
for (let i = 0; i < cells.length; i++) {
|
|
859
|
+
const cell = cells[i];
|
|
860
|
+
if (cell?.state?.type !== 'rack-grid-cell')
|
|
861
|
+
continue;
|
|
862
|
+
const bayKey = cell.state.cellId;
|
|
863
|
+
if (!bayKey)
|
|
864
|
+
continue;
|
|
865
|
+
const parsed = this.parsePosKey(bayKey);
|
|
866
|
+
if (!parsed)
|
|
867
|
+
continue;
|
|
868
|
+
for (let shelfIdx = 0; shelfIdx < shelves; shelfIdx++) {
|
|
869
|
+
const cellId = `${parsed.col}-${parsed.row}-${shelfIdx}`;
|
|
870
|
+
const loc = this.locationOf(cellId);
|
|
871
|
+
if (loc)
|
|
872
|
+
map.set(loc, cellId);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// 2. cellOverrides fallback (cell-component 없는 시점 — 호환)
|
|
877
|
+
const overrides = this.cellOverrides;
|
|
878
|
+
for (const posKey of Object.keys(overrides)) {
|
|
879
|
+
const override = overrides[posKey];
|
|
880
|
+
if (!override.section || !override.unit || override.isEmpty)
|
|
881
|
+
continue;
|
|
882
|
+
const parsed = this.parsePosKey(posKey);
|
|
883
|
+
if (!parsed)
|
|
884
|
+
continue;
|
|
885
|
+
for (let shelfIdx = 0; shelfIdx < shelves; shelfIdx++) {
|
|
886
|
+
const cellId = `${parsed.col}-${parsed.row}-${shelfIdx}`;
|
|
887
|
+
if (!map.has(this.locationOf(cellId) || '')) {
|
|
888
|
+
const loc = this.locationOf(cellId);
|
|
889
|
+
if (loc && !map.has(loc))
|
|
890
|
+
map.set(loc, cellId);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
this._locationIndexCache = map;
|
|
895
|
+
return map;
|
|
896
|
+
}
|
|
897
|
+
/** 룰 / cellOverrides 변경 시 호출. things-scene 의 onchange* 가 자동 호출. */
|
|
898
|
+
invalidateLocationIndex() {
|
|
899
|
+
this._locationIndexCache = undefined;
|
|
900
|
+
}
|
|
901
|
+
onchangeCellOverrides() { this.invalidateLocationIndex(); }
|
|
902
|
+
onchangeLocPattern() { this.invalidateLocationIndex(); }
|
|
903
|
+
onchangeShelfLocations() { this.invalidateLocationIndex(); }
|
|
904
|
+
onchangeZone() { this.invalidateLocationIndex(); }
|
|
905
|
+
onchangeSectionDigits() { this.invalidateLocationIndex(); }
|
|
906
|
+
onchangeUnitDigits() { this.invalidateLocationIndex(); }
|
|
907
|
+
onchangeShelves() { this.invalidateLocationIndex(); }
|
|
908
|
+
// ── 자식 lookup ────────────────────────────────────────
|
|
909
|
+
/**
|
|
910
|
+
* (col, row) 위치의 자식 StorageRack. 자식이 StorageRack 이 아닌 경우 (Crane, AGV 등)
|
|
911
|
+
* 는 null — Carrier 보관은 StorageRack 만.
|
|
912
|
+
*
|
|
913
|
+
* 자식의 grid 위치 식별: 자식의 state.column / state.row (1-based) 명시. RackGrid 가
|
|
914
|
+
* 자식 추가 시 자동 할당 또는 사용자가 명시.
|
|
915
|
+
*/
|
|
916
|
+
_childRackAt(posKey) {
|
|
917
|
+
const parsed = this.parsePosKey(posKey);
|
|
918
|
+
if (!parsed)
|
|
919
|
+
return null;
|
|
920
|
+
const children = this.components ?? [];
|
|
921
|
+
for (const c of children) {
|
|
922
|
+
const type = c.state?.type;
|
|
923
|
+
if (type !== 'storage-rack')
|
|
924
|
+
continue;
|
|
925
|
+
const childCol = (c.state?.column ?? 1) - 1;
|
|
926
|
+
const childRow = (c.state?.row ?? 1) - 1;
|
|
927
|
+
if (childCol === parsed.col && childRow === parsed.row) {
|
|
928
|
+
return c;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
// ── SlottedHolder 컨트랙 — 자식 StorageRack 으로 위임 ────
|
|
934
|
+
hasCarrierAt(slotId) {
|
|
935
|
+
const parsed = this.parseCellId(slotId);
|
|
936
|
+
if (!parsed)
|
|
937
|
+
return false;
|
|
938
|
+
const child = this._childRackAt(`${parsed.col}-${parsed.row}`);
|
|
939
|
+
return child?.hasCarrierAt(`0-0-${parsed.shelf}`) ?? false;
|
|
940
|
+
}
|
|
941
|
+
obtainCarrier(slotIdOrLocation) {
|
|
942
|
+
const slotId = this._resolveToCellId(slotIdOrLocation);
|
|
943
|
+
if (!slotId)
|
|
944
|
+
return null;
|
|
945
|
+
const parsed = this.parseCellId(slotId);
|
|
946
|
+
if (!parsed)
|
|
947
|
+
return null;
|
|
948
|
+
const child = this._childRackAt(`${parsed.col}-${parsed.row}`);
|
|
949
|
+
return child?.obtainCarrier(`0-0-${parsed.shelf}`) ?? null;
|
|
950
|
+
}
|
|
951
|
+
canReceiveAt(slotIdOrLocation, carrier) {
|
|
952
|
+
const slotId = this._resolveToCellId(slotIdOrLocation);
|
|
953
|
+
if (!slotId)
|
|
954
|
+
return false;
|
|
955
|
+
const parsed = this.parseCellId(slotId);
|
|
956
|
+
if (!parsed)
|
|
957
|
+
return false;
|
|
958
|
+
// isEmpty 위치 는 carrier 못 받음 (location-less, modeling 차원)
|
|
959
|
+
const override = this.cellOverrides[`${parsed.col}-${parsed.row}`];
|
|
960
|
+
if (override?.isEmpty)
|
|
961
|
+
return false;
|
|
962
|
+
const child = this._childRackAt(`${parsed.col}-${parsed.row}`);
|
|
963
|
+
return child?.canReceiveAt(`0-0-${parsed.shelf}`, carrier) ?? false;
|
|
964
|
+
}
|
|
965
|
+
async receiveAt(slotIdOrLocation, carrier, options) {
|
|
966
|
+
const slotId = this._resolveToCellId(slotIdOrLocation);
|
|
967
|
+
if (!slotId)
|
|
968
|
+
throw new Error(`RackGrid.receiveAt: cannot resolve "${slotIdOrLocation}"`);
|
|
969
|
+
const parsed = this.parseCellId(slotId);
|
|
970
|
+
if (!parsed)
|
|
971
|
+
throw new Error(`RackGrid.receiveAt: invalid cellId "${slotId}"`);
|
|
972
|
+
const child = this._childRackAt(`${parsed.col}-${parsed.row}`);
|
|
973
|
+
if (!child) {
|
|
974
|
+
throw new Error(`RackGrid.receiveAt: no StorageRack at posKey=${parsed.col}-${parsed.row}`);
|
|
975
|
+
}
|
|
976
|
+
return child.receiveAt(`0-0-${parsed.shelf}`, carrier, options);
|
|
977
|
+
}
|
|
978
|
+
recordFromCarrier(carrier, slotId) {
|
|
979
|
+
const { id, refid, transform, ...rest } = (carrier.state || {});
|
|
980
|
+
return { ...rest, cellId: slotId };
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Slot 의 attach object3d — Stock InstancedMesh 의 instance 와 *같은 world 위치* 에
|
|
984
|
+
* 위치한 invisible Object3D. popup tether / Carriable.applyHolderAttachPoint 가 이
|
|
985
|
+
* object3d 의 matrixWorld 를 사용. lazy 생성 + cache.
|
|
986
|
+
*/
|
|
987
|
+
_attachAnchorByCell = new Map();
|
|
988
|
+
getSlotAttachObject3d(slotId) {
|
|
989
|
+
const parsed = this.parseCellId(slotId);
|
|
990
|
+
if (!parsed)
|
|
991
|
+
return undefined;
|
|
992
|
+
if (parsed.col < 0 || parsed.col >= this.columns)
|
|
993
|
+
return undefined;
|
|
994
|
+
if (parsed.row < 0 || parsed.row >= this.rackRows)
|
|
995
|
+
return undefined;
|
|
996
|
+
if (parsed.shelf < 0 || parsed.shelf >= this.shelves)
|
|
997
|
+
return undefined;
|
|
998
|
+
const ro = this._realObject;
|
|
999
|
+
if (!ro?.object3d)
|
|
1000
|
+
return undefined;
|
|
1001
|
+
let obj = this._attachAnchorByCell.get(slotId);
|
|
1002
|
+
if (!obj) {
|
|
1003
|
+
obj = new THREE.Object3D();
|
|
1004
|
+
obj.name = `rack-grid-anchor:${slotId}`;
|
|
1005
|
+
ro.object3d.add(obj);
|
|
1006
|
+
this._attachAnchorByCell.set(slotId, obj);
|
|
1007
|
+
}
|
|
1008
|
+
// 위치 갱신 — Stock InstancedMesh 의 instance 위치 와 동일 공식
|
|
1009
|
+
const rs = this.state;
|
|
1010
|
+
const cols = this.columns;
|
|
1011
|
+
const rows = this.rackRows;
|
|
1012
|
+
const shelves = this.shelves;
|
|
1013
|
+
const width = rs?.width ?? 400;
|
|
1014
|
+
const height = rs?.depth ?? 2000;
|
|
1015
|
+
const depth = rs?.height ?? 200;
|
|
1016
|
+
const shelfBase = Math.max(0, Math.min(rs?.shelfBaseHeight || 0, height * 0.9));
|
|
1017
|
+
const shelfZone = height - shelfBase;
|
|
1018
|
+
const bayW = width / cols;
|
|
1019
|
+
const bayD = depth / rows;
|
|
1020
|
+
const cellY = shelfZone / shelves;
|
|
1021
|
+
const baseY = -height / 2;
|
|
1022
|
+
const shelfBaseY = baseY + shelfBase;
|
|
1023
|
+
const stockD = cellY * 0.7;
|
|
1024
|
+
const cx = (parsed.col - cols / 2 + 0.5) * bayW;
|
|
1025
|
+
const cellBottomY = shelfBaseY + parsed.shelf * cellY;
|
|
1026
|
+
const cy = cellBottomY + stockD / 2;
|
|
1027
|
+
const cz = (parsed.row - rows / 2 + 0.5) * bayD;
|
|
1028
|
+
obj.position.set(cx, cy, cz);
|
|
1029
|
+
// *parent 의 matrixWorld 가 dirty 면 자식 matrixWorld 도 dirty* — 강제 갱신.
|
|
1030
|
+
ro.object3d.updateMatrixWorld(true);
|
|
1031
|
+
obj.updateMatrixWorld(true);
|
|
1032
|
+
return obj;
|
|
1033
|
+
}
|
|
1034
|
+
getSlotSize(slotId) {
|
|
1035
|
+
const parsed = this.parseCellId(slotId);
|
|
1036
|
+
if (!parsed)
|
|
1037
|
+
return undefined;
|
|
1038
|
+
const rs = this.state;
|
|
1039
|
+
const width = rs?.width ?? 400;
|
|
1040
|
+
const height = rs?.depth ?? 2000;
|
|
1041
|
+
const depth = rs?.height ?? 200;
|
|
1042
|
+
const shelfBase = Math.max(0, Math.min(rs?.shelfBaseHeight || 0, height * 0.9));
|
|
1043
|
+
const shelfZone = height - shelfBase;
|
|
1044
|
+
const cellY = shelfZone / this.shelves;
|
|
811
1045
|
return {
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1046
|
+
width: (width / this.columns) * 0.85,
|
|
1047
|
+
height: (depth / this.rackRows) * 0.85, // 2D height = 3D Z 폭
|
|
1048
|
+
depth: cellY * 0.7 // 3D Y 폭 (= stockD)
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
cellCenter2D(slotId) {
|
|
1052
|
+
const parsed = this.parseCellId(slotId);
|
|
1053
|
+
if (!parsed)
|
|
1054
|
+
return null;
|
|
1055
|
+
const rs = this.state;
|
|
1056
|
+
const left = rs?.left ?? 0;
|
|
1057
|
+
const top = rs?.top ?? 0;
|
|
1058
|
+
const width = rs?.width ?? 400;
|
|
1059
|
+
const height = rs?.height ?? 200;
|
|
1060
|
+
// table layout 의 widths/heights 비례 위치
|
|
1061
|
+
const widths = this.widths;
|
|
1062
|
+
const heights = this.heights;
|
|
1063
|
+
const wSum = this.widths_sum;
|
|
1064
|
+
const hSum = this.heights_sum;
|
|
1065
|
+
let cumW = 0;
|
|
1066
|
+
for (let c = 0; c < parsed.col; c++)
|
|
1067
|
+
cumW += widths[c];
|
|
1068
|
+
let cumH = 0;
|
|
1069
|
+
for (let r = 0; r < parsed.row; r++)
|
|
1070
|
+
cumH += heights[r];
|
|
1071
|
+
const x = left + (cumW + widths[parsed.col] / 2) / wSum * width;
|
|
1072
|
+
const y = top + (cumH + heights[parsed.row] / 2) / hSum * height;
|
|
1073
|
+
return { x, y };
|
|
1074
|
+
}
|
|
1075
|
+
slotTargetAt(slotIdOrLocation) {
|
|
1076
|
+
const slotId = this._resolveToCellId(slotIdOrLocation);
|
|
1077
|
+
if (!slotId)
|
|
1078
|
+
throw new Error(`RackGrid.slotTargetAt: cannot resolve "${slotIdOrLocation}"`);
|
|
1079
|
+
return new SlotTarget(this, slotId);
|
|
1080
|
+
}
|
|
1081
|
+
// ── Helpers ──────────────────────────────────────────
|
|
1082
|
+
/**
|
|
1083
|
+
* 외부 입력 → 내부 cellId 변환:
|
|
1084
|
+
* - "N-N-N" (3 segments, 모두 숫자) → cellId 그대로 (dual-accept)
|
|
1085
|
+
* - 그 외 → location 으로 간주, locationIndex lookup
|
|
1086
|
+
*/
|
|
1087
|
+
_resolveToCellId(input) {
|
|
1088
|
+
if (this._isCellIdFormat(input))
|
|
1089
|
+
return input;
|
|
1090
|
+
return this.cellIdOfLocation(input);
|
|
1091
|
+
}
|
|
1092
|
+
_isCellIdFormat(s) {
|
|
1093
|
+
const parts = s.split('-');
|
|
1094
|
+
return parts.length === 3 && parts.every(p => /^\d+$/.test(p));
|
|
1095
|
+
}
|
|
1096
|
+
// ── Bulk 편집 API ────────────────────────────────────────
|
|
1097
|
+
//
|
|
1098
|
+
// 모두 *single setState* 로 cellOverrides 갱신 → 단일 'change' 이벤트만 발사.
|
|
1099
|
+
// 매핑 cascade 회피 위해 silent 갱신 메서드 제공 (`_setCellOverridesSilently`).
|
|
1100
|
+
/**
|
|
1101
|
+
* 한 위치의 cellOverride 를 갱신 (merge). 기존 필드는 유지, 새 필드만 덮어씀.
|
|
1102
|
+
* `partial` 의 필드가 *undefined* 면 *그 필드 삭제* (override 키에서 제거).
|
|
1103
|
+
*/
|
|
1104
|
+
setCellOverride(posKey, partial) {
|
|
1105
|
+
const current = this.cellOverrides;
|
|
1106
|
+
const existing = current[posKey] ?? {};
|
|
1107
|
+
const merged = { ...existing };
|
|
1108
|
+
for (const [k, v] of Object.entries(partial)) {
|
|
1109
|
+
if (v === undefined)
|
|
1110
|
+
delete merged[k];
|
|
1111
|
+
else
|
|
1112
|
+
merged[k] = v;
|
|
1113
|
+
}
|
|
1114
|
+
const next = { ...current };
|
|
1115
|
+
if (Object.keys(merged).length === 0)
|
|
1116
|
+
delete next[posKey];
|
|
1117
|
+
else
|
|
1118
|
+
next[posKey] = merged;
|
|
1119
|
+
this.setState({ cellOverrides: next });
|
|
1120
|
+
}
|
|
1121
|
+
/** 한 위치의 override 전체 삭제. */
|
|
1122
|
+
clearCellOverride(posKey) {
|
|
1123
|
+
const current = this.cellOverrides;
|
|
1124
|
+
if (!(posKey in current))
|
|
1125
|
+
return;
|
|
1126
|
+
const next = { ...current };
|
|
1127
|
+
delete next[posKey];
|
|
1128
|
+
this.setState({ cellOverrides: next });
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* 여러 위치의 isEmpty 일괄 토글. cell-component 있으면 cell.set(), 없으면 cellOverrides.
|
|
1132
|
+
*/
|
|
1133
|
+
setIsEmpty(posKeys, isEmpty) {
|
|
1134
|
+
const hasCells = this.components.some((c) => c?.state?.type === 'rack-grid-cell');
|
|
1135
|
+
if (hasCells) {
|
|
1136
|
+
for (const posKey of posKeys) {
|
|
1137
|
+
const cell = this.cellAtBayKey(posKey);
|
|
1138
|
+
cell?.set?.('isEmpty', isEmpty);
|
|
1139
|
+
}
|
|
1140
|
+
this.invalidateLocationIndex();
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
// Fallback: cellOverrides
|
|
1144
|
+
const current = this.cellOverrides;
|
|
1145
|
+
const next = { ...current };
|
|
1146
|
+
for (const posKey of posKeys) {
|
|
1147
|
+
const existing = next[posKey] ?? {};
|
|
1148
|
+
const merged = { ...existing, isEmpty };
|
|
1149
|
+
if (!isEmpty && !existing.section && !existing.unit && !existing.border && !existing.merged) {
|
|
1150
|
+
delete next[posKey];
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
next[posKey] = merged;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
this.setState({ cellOverrides: next });
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* 여러 위치의 border 스타일 일괄 설정. `where` ('top'|'left'|'bottom'|'right'|'all').
|
|
1160
|
+
* - style = null → 해당 side 삭제
|
|
1161
|
+
* - where = 'all' → 4면 모두 동일 style
|
|
1162
|
+
*/
|
|
1163
|
+
setBorder(posKeys, style, where = 'all') {
|
|
1164
|
+
const sides = where === 'all' ? ['top', 'left', 'bottom', 'right'] : [where];
|
|
1165
|
+
const current = this.cellOverrides;
|
|
1166
|
+
const next = { ...current };
|
|
1167
|
+
for (const posKey of posKeys) {
|
|
1168
|
+
const existing = next[posKey] ?? {};
|
|
1169
|
+
const border = { ...(existing.border ?? {}) };
|
|
1170
|
+
for (const side of sides) {
|
|
1171
|
+
if (style == null)
|
|
1172
|
+
delete border[side];
|
|
1173
|
+
else
|
|
1174
|
+
border[side] = style;
|
|
1175
|
+
}
|
|
1176
|
+
const merged = { ...existing, border };
|
|
1177
|
+
if (Object.keys(border).length === 0)
|
|
1178
|
+
delete merged.border;
|
|
1179
|
+
next[posKey] = merged;
|
|
1180
|
+
}
|
|
1181
|
+
this.setState({ cellOverrides: next });
|
|
1182
|
+
}
|
|
1183
|
+
/**
|
|
1184
|
+
* 자동 채번 — 선택된 cell 들에 section/unit 자동 부여.
|
|
1185
|
+
*
|
|
1186
|
+
* @param posKeys 채번 대상 (선택 영역). 빈 array 면 *전체 grid*.
|
|
1187
|
+
* @param direction 순회 방향:
|
|
1188
|
+
* 'cw' — 한 row 내 col 증가 → 다음 row 는 반대 방향 (boustrophedon)
|
|
1189
|
+
* 'ccw' — 반대 방향
|
|
1190
|
+
* 'zigzag' — col 우선 증가 (cw 의 90° 회전)
|
|
1191
|
+
* 'zigzag-reverse' — 반대
|
|
1192
|
+
* @param skipNumbering true 면 isEmpty cell 건너뜀 (단위 번호 안 증가).
|
|
1193
|
+
* @param startSection 첫 section 번호 (default 1)
|
|
1194
|
+
* @param startUnit 첫 unit 번호 (default 1)
|
|
1195
|
+
*
|
|
1196
|
+
* Aisle row (모든 cell isEmpty) 는 section 경계로 인식 — section 번호가 다음 row 에서 +1.
|
|
1197
|
+
*
|
|
1198
|
+
* setState 한 번 (cellOverrides 일괄 갱신) → location 역인덱스 1회 무효화.
|
|
1199
|
+
*/
|
|
1200
|
+
increaseLocation(posKeys, direction = 'cw', skipNumbering = false, startSection = 1, startUnit = 1) {
|
|
1201
|
+
const sectionDigits = Math.max(1, Math.floor(this.state.sectionDigits ?? 2));
|
|
1202
|
+
const unitDigits = Math.max(1, Math.floor(this.state.unitDigits ?? 2));
|
|
1203
|
+
// 대상 결정: posKeys 비었으면 전체 grid
|
|
1204
|
+
const targets = posKeys.length > 0
|
|
1205
|
+
? [...posKeys]
|
|
1206
|
+
: this._allPosKeys();
|
|
1207
|
+
// (1) row 별 분류 (2D 배열, [row][col] = posKey)
|
|
1208
|
+
const byRow = [];
|
|
1209
|
+
let maxRow = -1;
|
|
1210
|
+
let maxCol = -1;
|
|
1211
|
+
for (const posKey of targets) {
|
|
1212
|
+
const parsed = this.parsePosKey(posKey);
|
|
1213
|
+
if (!parsed)
|
|
1214
|
+
continue;
|
|
1215
|
+
if (!byRow[parsed.row])
|
|
1216
|
+
byRow[parsed.row] = [];
|
|
1217
|
+
byRow[parsed.row][parsed.col] = posKey;
|
|
1218
|
+
if (parsed.row > maxRow)
|
|
1219
|
+
maxRow = parsed.row;
|
|
1220
|
+
if (parsed.col > maxCol)
|
|
1221
|
+
maxCol = parsed.col;
|
|
1222
|
+
}
|
|
1223
|
+
// (2) aisle row 식별 — row 의 모든 (선택된) cell 이 isEmpty
|
|
1224
|
+
const overrides = this.cellOverrides;
|
|
1225
|
+
const isAisleRow = (row) => {
|
|
1226
|
+
const cells = byRow[row];
|
|
1227
|
+
if (!cells || cells.length === 0)
|
|
1228
|
+
return true;
|
|
1229
|
+
return cells.every(p => p == null || overrides[p]?.isEmpty === true);
|
|
1230
|
+
};
|
|
1231
|
+
// (3) section 별 묶기 (aisle 사이의 연속 row 들)
|
|
1232
|
+
const sections = [];
|
|
1233
|
+
let currentSection = [];
|
|
1234
|
+
for (let r = 0; r <= maxRow; r++) {
|
|
1235
|
+
if (!byRow[r])
|
|
1236
|
+
continue;
|
|
1237
|
+
if (isAisleRow(r)) {
|
|
1238
|
+
if (currentSection.length > 0) {
|
|
1239
|
+
sections.push(currentSection);
|
|
1240
|
+
currentSection = [];
|
|
815
1241
|
}
|
|
816
1242
|
}
|
|
1243
|
+
else {
|
|
1244
|
+
currentSection.push(r);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
if (currentSection.length > 0)
|
|
1248
|
+
sections.push(currentSection);
|
|
1249
|
+
// (4) 방향별 순회로 (section, unit) 부여
|
|
1250
|
+
const hasCells = this.components.some((c) => c?.state?.type === 'rack-grid-cell');
|
|
1251
|
+
const next = { ...overrides };
|
|
1252
|
+
let sectionNum = Number(startSection) || 1;
|
|
1253
|
+
const setSU = (posKey, section, unit) => {
|
|
1254
|
+
const sStr = String(section).padStart(sectionDigits, '0');
|
|
1255
|
+
const uStr = String(unit).padStart(unitDigits, '0');
|
|
1256
|
+
if (hasCells) {
|
|
1257
|
+
const cell = this.cellAtBayKey(posKey);
|
|
1258
|
+
cell?.set?.('section', sStr);
|
|
1259
|
+
cell?.set?.('unit', uStr);
|
|
1260
|
+
}
|
|
1261
|
+
else {
|
|
1262
|
+
const existing = next[posKey] ?? {};
|
|
1263
|
+
next[posKey] = { ...existing, section: sStr, unit: uStr };
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
const clearSU = (posKey) => {
|
|
1267
|
+
if (hasCells) {
|
|
1268
|
+
const cell = this.cellAtBayKey(posKey);
|
|
1269
|
+
cell?.set?.('section', null);
|
|
1270
|
+
cell?.set?.('unit', null);
|
|
1271
|
+
}
|
|
1272
|
+
else {
|
|
1273
|
+
const existing = next[posKey];
|
|
1274
|
+
if (!existing)
|
|
1275
|
+
return;
|
|
1276
|
+
const { section, unit, ...rest } = existing;
|
|
1277
|
+
if (Object.keys(rest).length === 0)
|
|
1278
|
+
delete next[posKey];
|
|
1279
|
+
else
|
|
1280
|
+
next[posKey] = rest;
|
|
1281
|
+
}
|
|
817
1282
|
};
|
|
1283
|
+
for (const sectionRows of sections) {
|
|
1284
|
+
for (const row of sectionRows) {
|
|
1285
|
+
const cells = byRow[row];
|
|
1286
|
+
if (!cells)
|
|
1287
|
+
continue;
|
|
1288
|
+
// 방향별 cell 순회 순서 결정
|
|
1289
|
+
const orderedCols = this._orderCols(row, sectionRows, direction, maxCol);
|
|
1290
|
+
let unitNum = Number(startUnit) || 1;
|
|
1291
|
+
for (const col of orderedCols) {
|
|
1292
|
+
const posKey = cells[col];
|
|
1293
|
+
if (!posKey)
|
|
1294
|
+
continue;
|
|
1295
|
+
const isEmpty = overrides[posKey]?.isEmpty === true;
|
|
1296
|
+
if (isEmpty) {
|
|
1297
|
+
clearSU(posKey);
|
|
1298
|
+
if (!skipNumbering)
|
|
1299
|
+
unitNum++;
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
setSU(posKey, sectionNum, unitNum);
|
|
1303
|
+
unitNum++;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
// row 마다 section 증가? 아님 — section 은 *aisle 로 구분된 묶음 단위*.
|
|
1307
|
+
// 동일 section 내 다음 row 는 unit 번호 다시 startUnit 부터.
|
|
1308
|
+
// rack-table 의 원본 동작과 동일.
|
|
1309
|
+
}
|
|
1310
|
+
sectionNum++;
|
|
1311
|
+
}
|
|
1312
|
+
// cell-component 가 있으면 cell.set() 으로 이미 갱신됨 (setSU/clearSU 안에서).
|
|
1313
|
+
// 없으면 cellOverrides fallback 적용.
|
|
1314
|
+
if (!hasCells) {
|
|
1315
|
+
this.setState({ cellOverrides: next });
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
this.invalidateLocationIndex();
|
|
1319
|
+
}
|
|
818
1320
|
}
|
|
819
|
-
|
|
820
|
-
|
|
1321
|
+
/**
|
|
1322
|
+
* 모든 grid 위치 (전체 columns × rows) 의 posKey 목록.
|
|
1323
|
+
*/
|
|
1324
|
+
_allPosKeys() {
|
|
1325
|
+
const cols = this.columns;
|
|
1326
|
+
const rows = this.rackRows;
|
|
1327
|
+
const out = [];
|
|
1328
|
+
for (let r = 0; r < rows; r++) {
|
|
1329
|
+
for (let c = 0; c < cols; c++) {
|
|
1330
|
+
out.push(`${c}-${r}`);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return out;
|
|
1334
|
+
}
|
|
1335
|
+
/**
|
|
1336
|
+
* Row 의 col 순회 순서 결정. boustrophedon (S 자형) 패턴 지원.
|
|
1337
|
+
* cw : 짝수 row 는 좌→우, 홀수 row 는 우→좌
|
|
1338
|
+
* ccw : 짝수 row 는 우→좌, 홀수 row 는 좌→우
|
|
1339
|
+
* zigzag : 모든 row 좌→우 (단순)
|
|
1340
|
+
* zigzag-reverse : 모든 row 우→좌
|
|
1341
|
+
*/
|
|
1342
|
+
_orderCols(row, sectionRows, direction, maxCol) {
|
|
1343
|
+
// section 내 row 의 *상대 인덱스* — section 경계에서 패턴이 리셋되도록.
|
|
1344
|
+
const idxInSection = sectionRows.indexOf(row);
|
|
1345
|
+
const cols = [];
|
|
1346
|
+
for (let c = 0; c <= maxCol; c++)
|
|
1347
|
+
cols.push(c);
|
|
1348
|
+
switch (direction) {
|
|
1349
|
+
case 'cw':
|
|
1350
|
+
return idxInSection % 2 === 0 ? cols : cols.slice().reverse();
|
|
1351
|
+
case 'ccw':
|
|
1352
|
+
return idxInSection % 2 === 0 ? cols.slice().reverse() : cols;
|
|
1353
|
+
case 'zigzag':
|
|
1354
|
+
return cols;
|
|
1355
|
+
case 'zigzag-reverse':
|
|
1356
|
+
return cols.slice().reverse();
|
|
1357
|
+
}
|
|
821
1358
|
}
|
|
822
1359
|
};
|
|
823
1360
|
RackGrid = __decorate([
|
|
824
1361
|
sceneComponent('rack-grid')
|
|
825
1362
|
], RackGrid);
|
|
826
1363
|
export default RackGrid;
|
|
827
|
-
;
|
|
828
|
-
['rows', 'columns', 'widths', 'heights', 'widths_sum', 'heights_sum', 'controls'].forEach(getter => Component.memoize(RackGrid.prototype, getter, false));
|
|
829
1364
|
//# sourceMappingURL=rack-grid.js.map
|