@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/MIGRATION-plan-a-slot-api.md +266 -0
  3. package/PLAN-A-rack-as-slot-holder.md +164 -0
  4. package/dist/crane.js +1 -1
  5. package/dist/crane.js.map +1 -1
  6. package/dist/index.d.ts +3 -4
  7. package/dist/index.js +1 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/rack-grid-3d.d.ts +18 -7
  10. package/dist/rack-grid-3d.js +372 -69
  11. package/dist/rack-grid-3d.js.map +1 -1
  12. package/dist/rack-grid-cell.d.ts +21 -72
  13. package/dist/rack-grid-cell.js +147 -243
  14. package/dist/rack-grid-cell.js.map +1 -1
  15. package/dist/rack-grid.d.ts +277 -56
  16. package/dist/rack-grid.js +1230 -695
  17. package/dist/rack-grid.js.map +1 -1
  18. package/dist/rack-materials.d.ts +9 -0
  19. package/dist/rack-materials.js +55 -0
  20. package/dist/rack-materials.js.map +1 -0
  21. package/dist/storage-rack-3d.d.ts +15 -0
  22. package/dist/storage-rack-3d.js +131 -30
  23. package/dist/storage-rack-3d.js.map +1 -1
  24. package/dist/storage-rack.d.ts +242 -45
  25. package/dist/storage-rack.js +684 -106
  26. package/dist/storage-rack.js.map +1 -1
  27. package/package.json +3 -3
  28. package/src/crane.ts +1 -1
  29. package/src/index.ts +3 -4
  30. package/src/rack-grid-3d.ts +383 -80
  31. package/src/rack-grid-cell.ts +161 -305
  32. package/src/rack-grid.ts +1263 -762
  33. package/src/rack-materials.ts +61 -0
  34. package/src/storage-rack-3d.ts +144 -30
  35. package/src/storage-rack.ts +763 -111
  36. package/test/test-carrier-lifecycle.ts +361 -0
  37. package/test/test-coord-alignment.ts +201 -0
  38. package/test/test-external-to-rack.ts +461 -0
  39. package/test/test-mover-concurrent-bug.ts +304 -0
  40. package/test/test-mover-rollback.ts +290 -0
  41. package/test/test-r19-place-absorb.ts +174 -0
  42. package/test/test-rack-3d-attach-real.ts +301 -0
  43. package/test/test-rack-concurrent.ts +254 -0
  44. package/test/test-rack-edge-cases.ts +323 -0
  45. package/test/test-rack-grid-cell.ts +318 -0
  46. package/test/test-rack-grid-location.ts +657 -0
  47. package/test/test-real-3d-positioning.ts +158 -0
  48. package/test/test-slot-center-convention.ts +116 -0
  49. package/test/test-slot-target.ts +189 -0
  50. package/test/test-storage-rack-batched.ts +606 -0
  51. package/test/test-storage-rack-click.ts +329 -0
  52. package/test/test-storage-rack-slot-api.ts +357 -0
  53. package/test/test-toscene-convention.ts +162 -0
  54. package/test/test-user-scenario-sequential.ts +334 -0
  55. package/translations/en.json +2 -0
  56. package/translations/ja.json +2 -0
  57. package/translations/ko.json +2 -0
  58. package/translations/ms.json +2 -0
  59. package/translations/zh.json +2 -0
  60. package/tsconfig.tsbuildinfo +1 -1
  61. package/dist/rack-column.d.ts +0 -35
  62. package/dist/rack-column.js +0 -258
  63. package/dist/rack-column.js.map +0 -1
  64. package/dist/rack-grid-helpers.d.ts +0 -28
  65. package/dist/rack-grid-helpers.js +0 -71
  66. package/dist/rack-grid-helpers.js.map +0 -1
  67. package/dist/rack-grid-location.d.ts +0 -37
  68. package/dist/rack-grid-location.js +0 -227
  69. package/dist/rack-grid-location.js.map +0 -1
  70. package/dist/storage-cell-3d.d.ts +0 -25
  71. package/dist/storage-cell-3d.js +0 -88
  72. package/dist/storage-cell-3d.js.map +0 -1
  73. package/dist/storage-cell.d.ts +0 -73
  74. package/dist/storage-cell.js +0 -215
  75. package/dist/storage-cell.js.map +0 -1
  76. package/src/rack-column.ts +0 -340
  77. package/src/rack-grid-helpers.ts +0 -77
  78. package/src/rack-grid-location.ts +0 -286
  79. package/src/storage-cell-3d.ts +0 -101
  80. package/src/storage-cell.ts +0 -267
  81. package/test/test-cell-position.ts +0 -105
  82. package/test/test-rack-grid.ts +0 -77
@@ -1,95 +1,94 @@
1
1
  /*
2
2
  * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * RackGridCell — RackGrid 의 *configuration storage* 단위 = 한 bay 의 메타데이터 핸들.
5
+ *
6
+ * 모드별 역할:
7
+ * - modeling (2D editor): 자체 사각형 render + click + nature.properties UI
8
+ * (rack-table-cell 패턴 그대로)
9
+ * - 2D / 3D view: *data holder 만* — render/mesh 모두 X. 외부 location API 가
10
+ * 이 cell 의 state.section/unit/isEmpty 등을 *configuration source of truth* 로 lookup.
11
+ *
12
+ * 3D mesh 는 *없음* (buildRealObject = undefined). 3D stock 시각화는 *RackGrid
13
+ * 자체의 RackGrid3D + InstancedMesh* 가 처리. cell-component 는 *3D 리소스 0*.
14
+ *
15
+ * 영속 데이터: cellId (bay 위치), section, unit, shelfLocations, isEmpty, border 등.
16
+ * RackGrid 의 hierarchy override 가 *redundant 좌표 (left/top/width/height)* 만 제거 —
17
+ * 위 own 데이터는 그대로 직렬화 (저장/로드 시 복원).
3
18
  */
4
- import { Component, Properties, RectPath, sceneComponent, RealObject } from '@hatiolab/things-scene'
19
+
20
+ import {
21
+ Component, ComponentNature, Properties, RectPath, sceneComponent
22
+ } from '@hatiolab/things-scene'
5
23
  import type { State } from '@hatiolab/things-scene'
6
- import { Rack } from './rack-column.js'
7
24
 
8
- /** RackGridCell 컴포넌트 state */
9
25
  export interface RackGridCellState extends State {
10
- section?: string
11
- unit?: string
12
- shelfLocations?: string
13
- binLocations?: string
14
- isEmpty?: boolean
15
- border?: any
26
+ cellId?: string // bayKey 형식: `${col-1}-${row-1}` (0-based, 2 segments)
27
+ section?: string // location 의 {s}
28
+ unit?: string // location 의 {u}
29
+ shelfLocations?: string // cell 별 shelf 명시 (CSV); 미지정 시 parent.shelfLocations
30
+ binLocations?: string // cell 내 bin 명시 (CSV) — rack-table 호환
31
+ isEmpty?: boolean // 통로/공실 표시
32
+ border?: any // 4-side border (top/left/bottom/right)
16
33
  merged?: boolean
17
34
  rowspan?: number
18
35
  colspan?: number
19
36
  }
20
37
 
21
38
  const EMPTY_BORDER = {}
39
+ const EMPTY_CELL_FILL = '#efefef'
40
+ const EMPTY_CELL_STROKE = '#ccc'
22
41
 
23
- function isBottomMost(idx: number, rows: number, columns: number) {
24
- return idx >= (rows - 1) * columns
42
+ function hasAnyProperty(o: any, ...props: string[]): boolean {
43
+ for (const p of props) if (Object.prototype.hasOwnProperty.call(o, p)) return true
44
+ return false
25
45
  }
26
46
 
27
- function isRightMost(idx: number, rows: number, columns: number) {
28
- return (idx + 1) % columns == 0
47
+ function isRightMost(idx: number, _rows: number, columns: number): boolean {
48
+ return (idx + 1) % columns === 0
29
49
  }
30
-
31
- function hasAnyProperty(o: any, ...properties: string[]) {
32
- for (let p in properties) {
33
- if (o.hasOwnProperty(properties[p])) {
34
- return true
35
- }
36
- }
50
+ function isBottomMost(idx: number, rows: number, columns: number): boolean {
51
+ return idx >= (rows - 1) * columns
37
52
  }
38
53
 
39
- const EMPTY_CELL_STROKE_STYLE = '#ccc'
40
- const EMPTY_CELL_LINE_WIDTH = 1
41
- const EMPTY_CELL_FILL_STYLE = '#efefef'
54
+ // RackGridCell refid 충돌 회피 — load 시 root.onadded → _addTraverse 가 자식부터
55
+ // addRefidIndex 호출. cell 의 model.refid 가 *비어있으면* root.getNewRefid() = 1,2,3...
56
+ // 작은 부여 → 부모 RackGrid (refid 7) / Crane (202) 등 *기존 refid 와 충돌*.
57
+ // constructor 에서 *큰 시작값 + monotonic counter* 로 자체 부여 → 충돌 0.
58
+ let _cellRefidCounter = 100_000_000
42
59
 
43
- /**
44
- * 1. 스타일을 상속 받아야 함. (cascade-style)
45
- * 2. 스타일을 동적처리할 수 있음. (로직처리)
46
- * 3. 데이타를 받을 수 있음.
47
- */
48
60
  @sceneComponent('rack-grid-cell')
49
- export class RackGridCell extends RectPath(Component) {
61
+ export default class RackGridCell extends RectPath(Component) {
50
62
  override get state(): RackGridCellState {
51
63
  return super.state as RackGridCellState
52
64
  }
53
65
 
54
66
  _focused: boolean = false
55
67
 
68
+ constructor(model: any, context: any) {
69
+ super(model, context)
70
+ // refid 미부여 시 큰 값 자체 부여 — root.getNewRefid() 의 1,2,3... 충돌 회피.
71
+ // hierarchy override 가 save 시 refid 제거 → load 마다 fresh.
72
+ if (this.model.refid == null) {
73
+ this.model.refid = _cellRefidCounter++
74
+ }
75
+ }
76
+
56
77
  get hasTextProperty() {
57
78
  return false
58
79
  }
59
80
 
60
- get nature() {
81
+ get nature(): ComponentNature {
61
82
  return {
62
83
  mutable: false,
63
84
  resizable: true,
64
85
  rotatable: true,
65
86
  properties: [
66
- {
67
- type: 'string',
68
- label: 'section',
69
- name: 'section'
70
- },
71
- {
72
- type: 'string',
73
- label: 'unit',
74
- name: 'unit'
75
- },
76
- {
77
- type: 'string',
78
- label: 'shelf-locations',
79
- name: 'shelfLocations',
80
- placeholder: '1,2,3,... / ,,,04'
81
- },
82
- {
83
- type: 'textarea',
84
- label: 'bin-locations',
85
- name: 'binLocations',
86
- placeholder: '1,2,3,...'
87
- },
88
- {
89
- type: 'checkbox',
90
- label: 'is-empty',
91
- name: 'isEmpty'
92
- },
87
+ { type: 'string', label: 'section', name: 'section' },
88
+ { type: 'string', label: 'unit', name: 'unit' },
89
+ { type: 'string', label: 'shelf-locations', name: 'shelfLocations', placeholder: '1,2,3 또는 ,,,04' },
90
+ { type: 'textarea', label: 'bin-locations', name: 'binLocations', placeholder: '1,2,3,...' },
91
+ { type: 'checkbox', label: 'is-empty', name: 'isEmpty' },
93
92
  {
94
93
  type: 'location-increase-pattern',
95
94
  label: '',
@@ -98,8 +97,16 @@ export class RackGridCell extends RectPath(Component) {
98
97
  event: {
99
98
  'increase-location-pattern': (event: CustomEvent) => {
100
99
  const { increasingDirection, skipNumbering, startSection, startUnit } = event.detail
101
- const rackTable = this.parent as any
102
- rackTable.increaseLocation(increasingDirection, skipNumbering, startSection, startUnit)
100
+ const parent: any = this.parent
101
+ if (typeof parent?.increaseLocation === 'function') {
102
+ // RackGrid 의 root.selected 활용 — framework 가 자동 갱신
103
+ const selected = (this.root as any)?.selected as RackGridCell[] | undefined
104
+ const keys = (selected || [])
105
+ .filter(c => (c as any)?.state?.type === 'rack-grid-cell')
106
+ .map(c => c.state.cellId)
107
+ .filter((k): k is string => !!k)
108
+ parent.increaseLocation(keys, increasingDirection, skipNumbering, startSection, startUnit)
109
+ }
103
110
  }
104
111
  }
105
112
  }
@@ -108,297 +115,146 @@ export class RackGridCell extends RectPath(Component) {
108
115
  type: 'editor-table',
109
116
  label: '',
110
117
  name: '',
111
- property: {
112
- merge: false,
113
- split: false
114
- }
118
+ property: { merge: false, split: false }
115
119
  }
116
120
  ]
117
121
  }
118
122
  }
119
123
 
120
- buildRealObject(): RealObject | undefined {
121
- // isEmpty cell 은 3D 에 mesh 를 만들지 않는다. v9 에서는 RackGrid3D.createRacks 가
122
- // cell._realObject 를 사전 설정하면 3D pipeline 이 자동 buildRealObject 호출을 skip
123
- // 하던 것을, v10 의 ThreeCapability 도입 이후로는 *항상* 자동 호출됨. RackGrid3D
124
- // createRacks 의 isEmpty 체크와 이 자동 경로 사이의 불일치로 isEmpty=true cell 도
125
- // 3D 에 그려지던 regression — 여기 isEmpty 체크 추가로 두 경로 정합.
126
- if (this.getState('isEmpty')) return undefined
127
- return new Rack(this)
124
+ /** 3D mesh 미생성 — view mode 의 리소스 0. */
125
+ buildRealObject(): undefined {
126
+ return undefined
128
127
  }
129
128
 
130
- set merged(merged) {
131
- this.set('merged', !!merged)
132
- if (merged) this.set('text', '')
133
- }
134
-
135
- get merged() {
136
- return this.getState('merged')
137
- }
129
+ // ── Domain getters ────────────────────────────────────
138
130
 
139
- set rowspan(rowspan) {
140
- this.set('rowspan', rowspan)
131
+ get isEmpty(): boolean {
132
+ return !!this.getState('isEmpty')
141
133
  }
142
134
 
143
- get rowspan() {
144
- return this.getState('rowspan')
145
- }
146
-
147
- set colspan(colspan) {
148
- this.set('colspan', colspan)
149
- }
150
-
151
- get colspan() {
152
- return this.getState('colspan')
153
- }
154
-
155
- get border() {
135
+ get border(): any {
156
136
  return this.state.border || EMPTY_BORDER
157
137
  }
158
138
 
159
- get isEmpty() {
160
- return this.getState('isEmpty')
139
+ set merged(v: boolean) {
140
+ this.set('merged', !!v)
141
+ if (v) this.set('text', '')
161
142
  }
162
-
163
- _drawBorder(context: CanvasRenderingContext2D, x: number, y: number, to_x: number, to_y: number, style: any) {
164
- if (style && style.strokeStyle && style.lineWidth && style.lineDash) {
165
- context.beginPath()
166
- context.moveTo(x, y)
167
- context.lineTo(to_x, to_y)
168
-
169
- Component.drawStroke(context, style)
170
- }
143
+ get merged(): boolean {
144
+ return !!this.getState('merged')
171
145
  }
172
146
 
173
- render(context: CanvasRenderingContext2D) {
174
- const { left, top, width, height } = this.bounds
175
- const { isEmpty } = this.state
147
+ set rowspan(v: number) { this.set('rowspan', v) }
148
+ get rowspan(): number { return (this.getState('rowspan') as number) ?? 1 }
176
149
 
177
- const border = this.border
150
+ set colspan(v: number) { this.set('colspan', v) }
151
+ get colspan(): number { return (this.getState('colspan') as number) ?? 1 }
178
152
 
179
- if (!isEmpty) {
180
- this._draw_rack_cell(context)
181
- }
182
-
183
- // Cell 채우기.
184
- context.beginPath()
185
- context.lineWidth = 0
186
- context.rect(left, top, width, height)
187
-
188
- // Border 그리기
153
+ /** Parent (RackGrid) 안에서 자기 (col, row) 인덱스 — components 순서 = row * columns + col. */
154
+ get index(): { row: number; column: number } {
189
155
  const parent = this.parent as any
156
+ if (!parent) return { row: 0, column: 0 }
190
157
  const idx = parent.components.indexOf(this)
191
- const columns = parent.columns || 1
192
- const rows = parent.rows || 1
193
-
194
- this._drawBorder(context, left, top, left + width, top, border.top)
195
- this._drawBorder(context, left, top + height, left, top, border.left)
196
- if (isRightMost(idx, rows, columns))
197
- this._drawBorder(context, left + width, top, left + width, top + height, border.right)
198
- if (isBottomMost(idx, rows, columns))
199
- this._drawBorder(context, left + width, top + height, left, top + height, border.bottom)
158
+ const cols = parent.columns ?? 1
159
+ return { row: Math.floor(idx / cols), column: idx % cols }
200
160
  }
201
161
 
202
- _draw_rack_cell(context: CanvasRenderingContext2D) {
203
- const { left, top, width, height } = this.bounds
204
-
205
- context.save()
206
- context.fillStyle = EMPTY_CELL_FILL_STYLE
207
- context.fillRect(left, top, width, height)
208
-
209
- context.beginPath()
210
- context.lineWidth = EMPTY_CELL_LINE_WIDTH
211
- context.strokeStyle = EMPTY_CELL_STROKE_STYLE
212
-
213
- context.moveTo(left, top)
214
- context.lineTo(left + width, top + height)
215
- context.moveTo(left + width, top)
216
- context.lineTo(left, top + height)
217
-
218
- context.stroke()
219
- context.closePath()
220
- context.restore()
221
- }
222
-
223
- get decotag() {
224
- const rackTable = this.parent
225
- let { locPattern, zone = '' } = rackTable.model
226
-
227
- locPattern = locPattern.substring(0, locPattern.indexOf('{u}') + 3)
228
-
229
- let locationString = ''
230
- if (this.getState('section') && this.getState('unit'))
231
- locationString = locPattern
232
- .replace('{z}', zone)
233
- .replace('{s}', this.getState('section'))
234
- .replace('{u}', this.getState('unit'))
235
-
236
- return locationString || ''
237
- }
238
-
239
- get index() {
240
- const rackTable = this.parent as any
241
- const index = rackTable.components.indexOf(this)
242
-
243
- const rowIndex = Math.floor(index / rackTable.columns)
244
- const columnIndex = index % rackTable.columns
245
-
246
- return {
247
- row: rowIndex,
248
- column: columnIndex
249
- }
250
- }
251
-
252
- get rowIndex() {
253
- return this.index.row
254
- }
255
-
256
- get columnIndex() {
257
- return this.index.column
258
- }
162
+ get rowIndex(): number { return this.index.row }
163
+ get columnIndex(): number { return this.index.column }
259
164
 
260
- get leftCell() {
261
- const rackTable = this.parent as any
262
-
263
- const rowIndex = this.rowIndex
264
- const columnIndex = this.columnIndex
265
-
266
- if (columnIndex === 0) return null
267
-
268
- const leftCell = rackTable.components[rowIndex * rackTable.columns + columnIndex - 1]
269
-
270
- return leftCell
271
- }
272
-
273
- get rightCell() {
274
- const rackTable = this.parent as any
275
-
276
- const rowIndex = this.rowIndex
277
- const columnIndex = this.columnIndex
278
-
279
- if (columnIndex === rackTable.columns) return null
280
-
281
- const rightCell = rackTable.components[rowIndex * rackTable.columns + columnIndex + 1]
282
-
283
- return rightCell
284
- }
285
-
286
- get aboveCell() {
287
- const rackTable = this.parent as any
288
-
289
- const rowIndex = this.rowIndex
290
- const columnIndex = this.columnIndex
291
-
292
- if (rowIndex === 0) return null
293
-
294
- const aboveCell = rackTable.components[(rowIndex - 1) * rackTable.columns + columnIndex]
295
-
296
- return aboveCell
165
+ get rowCells(): Component[] {
166
+ const parent = this.parent as any
167
+ return parent?.getCellsByRow?.(this.rowIndex) ?? []
297
168
  }
298
-
299
- get belowCell() {
300
- const rackTable = this.parent as any
301
-
302
- const rowIndex = this.rowIndex
303
- const columnIndex = this.columnIndex
304
-
305
- if (rowIndex === rackTable.rows) return null
306
-
307
- const belowCell = rackTable.components[(rowIndex + 1) * rackTable.columns + columnIndex]
308
-
309
- return belowCell
169
+ get columnCells(): Component[] {
170
+ const parent = this.parent as any
171
+ return parent?.getCellsByColumn?.(this.columnIndex) ?? []
310
172
  }
311
-
312
- get rowCells() {
313
- const rackTable = this.parent as any
314
- return rackTable.getCellsByRow(this.rowIndex)
173
+ get notEmptyRowCells(): Component[] {
174
+ return this.rowCells.filter(c => !(c as any).getState?.('isEmpty'))
315
175
  }
316
-
317
- get columnCells() {
318
- const rackTable = this.parent as any
319
- return rackTable.getCellsByColumn(this.columnIndex)
176
+ get isAisle(): boolean {
177
+ return this.notEmptyRowCells.length === 0
320
178
  }
321
179
 
322
- get aboveRowCells() {
323
- let aboveCell = this.aboveCell
324
- while (1) {
325
- const aboveRowCells = aboveCell.notEmptyRowCells
326
-
327
- if (aboveRowCells.length > 0) return aboveRowCells
180
+ // ── Lifecycle ─────────────────────────────────────────
328
181
 
329
- aboveCell = aboveCell.aboveCell
182
+ onchange(after: Properties, _before: Properties) {
183
+ if (hasAnyProperty(after, 'isEmpty')) {
184
+ // isEmpty 토글 시 section/unit 클리어 (rack-table-cell 동작)
185
+ delete this.model.unit
186
+ delete this.model.section
330
187
  }
188
+ // Parent 의 location index 무효화 (RackGrid 가 onchange 다시 안 받으므로 자식이 알림)
189
+ const parent: any = this.parent
190
+ parent?.invalidateLocationIndex?.()
331
191
  }
332
192
 
333
- get lastUnit() {
334
- const rowCells = this.aboveRowCells
335
-
336
- for (let i = rowCells.length - 1; i > 0; i--) {
337
- const cell = rowCells[i]
193
+ onmouseenter() { this.trigger('deco') }
194
+ onmouseleave() { this.trigger('decoreset') }
338
195
 
339
- const unit = cell.getState('unit')
340
-
341
- if (unit) return Number(unit)
196
+ contains(x: number, y: number) {
197
+ const c = super.contains(x, y)
198
+ if (!c) {
199
+ this._focused = false
200
+ this.invalidate()
342
201
  }
343
-
344
- return 0
202
+ return c
345
203
  }
346
204
 
347
- get firstUnit() {
348
- const rowCells = this.aboveRowCells
205
+ // ── 2D rendering (modeling editor) ────────────────────
206
+ //
207
+ // *view mode* (= app.isViewMode) 면 안 그림 — *configuration storage 만*. modeling
208
+ // 시에만 사각형 + border + isEmpty 패턴 시각화.
349
209
 
350
- for (let i = 0; i < rowCells.length; i++) {
351
- const cell = rowCells[i]
210
+ render(context: CanvasRenderingContext2D) {
211
+ if ((this as any).app?.isViewMode) return
352
212
 
353
- const unit = cell.getState('unit')
213
+ const { left, top, width, height } = this.bounds
214
+ const { isEmpty } = this.state
215
+ const border = this.border
354
216
 
355
- if (unit) return Number(unit)
217
+ if (isEmpty) {
218
+ // 빈 cell — 회색 fill + 대각선
219
+ context.save()
220
+ context.fillStyle = EMPTY_CELL_FILL
221
+ context.fillRect(left, top, width, height)
222
+ context.strokeStyle = EMPTY_CELL_STROKE
223
+ context.lineWidth = 1
224
+ context.beginPath()
225
+ context.moveTo(left, top); context.lineTo(left + width, top + height)
226
+ context.moveTo(left + width, top); context.lineTo(left, top + height)
227
+ context.stroke()
228
+ context.restore()
356
229
  }
357
230
 
358
- return 0
359
- }
360
-
361
- get notEmptyRowCells() {
362
- return this.rowCells.filter((c: Component) => {
363
- return !c.getState('isEmpty')
364
- })
365
- }
366
-
367
- get emptyRowCells() {
368
- return this.rowCells.filter((c: Component) => {
369
- return c.getState('isEmpty')
370
- })
371
- }
372
-
373
- get isAisle() {
374
- return this.notEmptyRowCells.length === 0
375
- }
231
+ // 명시적 cell 영역 표시 (border default — 자식 cell 의 미니 frame)
232
+ context.beginPath()
233
+ context.lineWidth = 0
234
+ context.rect(left, top, width, height)
376
235
 
377
- onchange(after: Properties, before: Properties) {
378
- if (hasAnyProperty(after, 'isEmpty')) {
379
- // FIXME
380
- delete this.model.unit
381
- delete this.model.section
236
+ const parent = this.parent as any
237
+ const idx = parent?.components?.indexOf?.(this) ?? 0
238
+ const columns = parent?.columns ?? 1
239
+ const rows = parent?.rackRows ?? 1
240
+
241
+ this._drawSide(context, left, top, left + width, top, border.top)
242
+ this._drawSide(context, left, top + height, left, top, border.left)
243
+ if (isRightMost(idx, rows, columns)) {
244
+ this._drawSide(context, left + width, top, left + width, top + height, border.right)
382
245
  }
383
- }
384
-
385
- onmouseenter() {
386
- this.trigger('deco')
387
- }
388
-
389
- onmouseleave() {
390
- this.trigger('decoreset')
391
- }
392
-
393
- contains(x: number, y: number) {
394
- const contains = super.contains(x, y)
395
- if (!contains) {
396
- this._focused = false
397
- this.invalidate()
246
+ if (isBottomMost(idx, rows, columns)) {
247
+ this._drawSide(context, left + width, top + height, left, top + height, border.bottom)
398
248
  }
249
+ }
399
250
 
400
- return contains
251
+ private _drawSide(ctx: CanvasRenderingContext2D, x: number, y: number, tx: number, ty: number, style: any) {
252
+ if (!style?.strokeStyle || !style.lineWidth) return
253
+ ctx.beginPath()
254
+ ctx.moveTo(x, y)
255
+ ctx.lineTo(tx, ty)
256
+ Component.drawStroke(ctx, style)
401
257
  }
402
258
  }
403
259
 
404
- ;['border'].forEach(getter => Component.memoize(RackGridCell.prototype, getter, false))
260
+ ;['border'].forEach(g => Component.memoize(RackGridCell.prototype, g, false))