@operato/scene-storage 10.0.0-beta.48 → 10.0.0-beta.53
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 +36 -0
- package/dist/box.js +2 -2
- package/dist/box.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/pallet.js +2 -2
- package/dist/pallet.js.map +1 -1
- package/dist/parcel.js +2 -2
- package/dist/parcel.js.map +1 -1
- package/dist/picking-station-3d.d.ts +20 -0
- package/dist/picking-station-3d.js +162 -0
- package/dist/picking-station-3d.js.map +1 -0
- package/dist/picking-station.d.ts +50 -0
- package/dist/picking-station.js +186 -0
- package/dist/picking-station.js.map +1 -0
- package/dist/rack-capability.d.ts +11 -0
- package/dist/rack-capability.js +25 -0
- package/dist/rack-capability.js.map +1 -0
- package/dist/rack-grid.d.ts +4 -22
- package/dist/rack-grid.js +23 -115
- package/dist/rack-grid.js.map +1 -1
- package/dist/spot.d.ts +1 -0
- package/dist/spot.js +6 -2
- package/dist/spot.js.map +1 -1
- package/dist/stockpile-3d.d.ts +55 -0
- package/dist/stockpile-3d.js +387 -0
- package/dist/stockpile-3d.js.map +1 -0
- package/dist/stockpile-grid-3d.d.ts +30 -0
- package/dist/stockpile-grid-3d.js +301 -0
- package/dist/stockpile-grid-3d.js.map +1 -0
- package/dist/stockpile-grid.d.ts +85 -0
- package/dist/stockpile-grid.js +361 -0
- package/dist/stockpile-grid.js.map +1 -0
- package/dist/stockpile.d.ts +116 -0
- package/dist/stockpile.js +345 -0
- package/dist/stockpile.js.map +1 -0
- package/dist/storage-rack.d.ts +39 -44
- package/dist/storage-rack.js +71 -146
- package/dist/storage-rack.js.map +1 -1
- package/dist/templates/index.d.ts +80 -0
- package/dist/templates/index.js +7 -1
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/picking-station.d.ts +20 -0
- package/dist/templates/picking-station.js +22 -0
- package/dist/templates/picking-station.js.map +1 -0
- package/dist/templates/stockpile-grid.d.ts +37 -0
- package/dist/templates/stockpile-grid.js +38 -0
- package/dist/templates/stockpile-grid.js.map +1 -0
- package/dist/templates/stockpile.d.ts +29 -0
- package/dist/templates/stockpile.js +31 -0
- package/dist/templates/stockpile.js.map +1 -0
- package/package.json +3 -3
- package/src/box.ts +2 -1
- package/src/index.ts +14 -0
- package/src/pallet.ts +2 -1
- package/src/parcel.ts +2 -1
- package/src/picking-station-3d.ts +164 -0
- package/src/picking-station.ts +220 -0
- package/src/rack-capability.ts +26 -0
- package/src/rack-grid.ts +24 -108
- package/src/spot.ts +15 -1
- package/src/stockpile-3d.ts +412 -0
- package/src/stockpile-grid-3d.ts +327 -0
- package/src/stockpile-grid.ts +408 -0
- package/src/stockpile.ts +427 -0
- package/src/storage-rack.ts +82 -137
- package/src/templates/index.ts +7 -1
- package/src/templates/picking-station.ts +23 -0
- package/src/templates/stockpile-grid.ts +39 -0
- package/src/templates/stockpile.ts +32 -0
- package/test/test-rack-capability.ts +51 -0
- package/translations/en.json +23 -6
- package/translations/ja.json +23 -6
- package/translations/ko.json +22 -5
- package/translations/ms.json +23 -6
- package/translations/zh.json +22 -5
- package/tsconfig.tsbuildinfo +1 -1
package/src/storage-rack.ts
CHANGED
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
CellMap,
|
|
10
10
|
CarrierHolder,
|
|
11
11
|
Placeable,
|
|
12
|
+
RecordStorage,
|
|
12
13
|
SlotTarget,
|
|
14
|
+
componentBoundingBox,
|
|
13
15
|
type AttachFrame,
|
|
14
16
|
type Alignment,
|
|
15
17
|
type Heights,
|
|
@@ -19,6 +21,7 @@ import {
|
|
|
19
21
|
} from '@operato/scene-base'
|
|
20
22
|
|
|
21
23
|
import { StorageRack3D } from './storage-rack-3d.js'
|
|
24
|
+
import { rackAcceptsMoverTool } from './rack-capability.js'
|
|
22
25
|
|
|
23
26
|
/** Rack 컴포넌트 state */
|
|
24
27
|
export interface StorageRackState extends State {
|
|
@@ -164,7 +167,9 @@ function _nextCarrierRefid(): number {
|
|
|
164
167
|
*/
|
|
165
168
|
@sceneComponent('storage-rack')
|
|
166
169
|
export default class Rack
|
|
167
|
-
extends
|
|
170
|
+
extends RecordStorage<{ cellId: string; [key: string]: any }>()(
|
|
171
|
+
CellContainer(CarrierHolder(Placeable(ContainerAbstract)))
|
|
172
|
+
)
|
|
168
173
|
implements SlottedHolder
|
|
169
174
|
{
|
|
170
175
|
declare state: StorageRackState
|
|
@@ -173,19 +178,40 @@ export default class Rack
|
|
|
173
178
|
static align: Alignment = 'bottom'
|
|
174
179
|
static defaultDepth = (h: Heights) => h.ceiling - h.floor
|
|
175
180
|
|
|
181
|
+
// ── RecordStorage mixin hook overrides ────────────────────────────────
|
|
182
|
+
// record.cellId 가 slotId (mixin default 의 r.id 가 아닌 r.cellId).
|
|
183
|
+
_recordToSlotId(record: { cellId: string }): string {
|
|
184
|
+
return record.cellId ?? ''
|
|
185
|
+
}
|
|
186
|
+
// 3D rebuild 가 _realObject.rebuildStockMesh (mixin default 와 동일이지만 명시).
|
|
187
|
+
_rebuildVisual(): void {
|
|
188
|
+
;(this._realObject as any)?.rebuildStockMesh?.()
|
|
189
|
+
}
|
|
190
|
+
|
|
176
191
|
// Phase Auto-Nav (AN-PR-2) — Obstacle 자격.
|
|
177
192
|
get isObstacle(): boolean {
|
|
178
193
|
return (this.state as any)?.isObstacle !== false
|
|
179
194
|
}
|
|
180
195
|
obstacleBoundingBox(): { left: number; top: number; width: number; height: number; y?: number; zHeight?: number } | null {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
196
|
+
// scene-base 의 componentBoundingBox 위임 — rotation 적용된 AABB.
|
|
197
|
+
return componentBoundingBox(this)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 이 rack 에 *_놓으려는 mover_* 의 능력을 수용하는가. canAccept(carrier) 는 carrier
|
|
202
|
+
* 타입만 보지만, 이건 *_적재 mover 의 물리 능력_* 을 본다.
|
|
203
|
+
*
|
|
204
|
+
* rack 선반 적재는 *_높이 도달_* mover (crane / stacker / forklift) 의 몫이다.
|
|
205
|
+
* 평탄 데크 차량(agv-deck)은 바닥 운반 전용 — 선반에 직접 못 올린다. 따라서
|
|
206
|
+
* agv-deck 류는 거부 → transfer planner 가 자동으로 in-port 경유(환승)를 택한다.
|
|
207
|
+
*
|
|
208
|
+
* 거부 목록은 state.blockedTools 로 override (default ['agv-deck']). 향후 선반
|
|
209
|
+
* 높이(level) vs mover liftHeight 비교로 정교화 가능.
|
|
210
|
+
*/
|
|
211
|
+
canAcceptFromMover(mover: any): boolean {
|
|
212
|
+
const toolType = mover?.toolType ?? mover?.state?.toolType
|
|
213
|
+
const blocked = (this.state as any)?.blockedTools ?? ['agv-deck']
|
|
214
|
+
return rackAcceptsMoverTool(toolType, blocked)
|
|
189
215
|
}
|
|
190
216
|
|
|
191
217
|
get nature() {
|
|
@@ -309,10 +335,7 @@ export default class Rack
|
|
|
309
335
|
// - Place: await crane.place(c, destRack.slotTargetAt('B-0-0'))
|
|
310
336
|
// SlotTarget.receive → destRack.receiveAt → c dispose + record push
|
|
311
337
|
|
|
312
|
-
|
|
313
|
-
get records(): ReadonlyArray<{ cellId: string; [key: string]: any }> {
|
|
314
|
-
return (this.state.data as any) ?? []
|
|
315
|
-
}
|
|
338
|
+
// `records` getter — RecordStorage mixin 제공.
|
|
316
339
|
|
|
317
340
|
/**
|
|
318
341
|
* 1-based (bay, row, level) → 0-based cellId 문자열.
|
|
@@ -409,7 +432,7 @@ export default class Rack
|
|
|
409
432
|
/** cellId 에 carrier 가 있는가 — child carrier 또는 state.data record 어느 쪽이든. */
|
|
410
433
|
hasCarrierAt(cellId: string): boolean {
|
|
411
434
|
if (this._carrierChildAt(cellId)) return true
|
|
412
|
-
return this.records.some(r => r.cellId === cellId)
|
|
435
|
+
return this.records.some((r: any) => r.cellId === cellId)
|
|
413
436
|
}
|
|
414
437
|
|
|
415
438
|
/** cellId 매칭되는 rack 의 직접 자식 carrier (operation archetype). */
|
|
@@ -507,25 +530,8 @@ export default class Rack
|
|
|
507
530
|
return carrier
|
|
508
531
|
}
|
|
509
532
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
* `setState` 와 달리 *'change' 이벤트 / onchangeData / mapping cascade 를 우회*.
|
|
513
|
-
*
|
|
514
|
-
* 이유: mapping 시스템이 state.data 변경 시 *자동으로 script fire*. Plan A 의
|
|
515
|
-
* setState 가 그 cascade 를 트리거하면 사용자 script 가 *재귀적으로 자기 자신을 호출*
|
|
516
|
-
* 하는 회귀 (board 의 의도된 binding 일 수도, 우연일 수도) 발생.
|
|
517
|
-
*
|
|
518
|
-
* 대신 *직접 _state 갱신 + rebuildStockMesh 직접 호출* — 시각화는 갱신되지만 외부
|
|
519
|
-
* mapping 은 fire 안 됨. External (WMS / application setState) 호출은 그대로 setState
|
|
520
|
-
* 거치므로 그쪽 mapping 은 정상 동작.
|
|
521
|
-
*/
|
|
522
|
-
private _setDataSilently(newData: any[]): void {
|
|
523
|
-
const self = this as any
|
|
524
|
-
if (!self._state) self._state = {}
|
|
525
|
-
self._state.data = newData
|
|
526
|
-
self._cachedState = null // state getter 가 다음 read 때 fresh build
|
|
527
|
-
;(this._realObject as any)?.rebuildStockMesh?.()
|
|
528
|
-
}
|
|
533
|
+
// _setDataSilently — RecordStorage mixin 제공 (_rebuildVisual hook 통해
|
|
534
|
+
// rebuildStockMesh 호출). 동등 동작 + cachedState invalidate.
|
|
529
535
|
|
|
530
536
|
/**
|
|
531
537
|
* cell 이 carrier 를 받을 수 있는가.
|
|
@@ -802,88 +808,7 @@ export default class Rack
|
|
|
802
808
|
;(this._realObject as any)?.rebuildStockMesh?.()
|
|
803
809
|
}
|
|
804
810
|
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
private _legendTarget?: Component
|
|
808
|
-
|
|
809
|
-
/**
|
|
810
|
-
* Legend 컴포넌트 lookup. 우선순위:
|
|
811
|
-
* 1) state.legendTarget id 명시
|
|
812
|
-
* 2) scene 전체에서 `type='legend'` 첫 번째 컴포넌트 (자동 발견)
|
|
813
|
-
*/
|
|
814
|
-
get legendTarget(): Component | undefined {
|
|
815
|
-
if (this._legendTarget) return this._legendTarget
|
|
816
|
-
|
|
817
|
-
const id = this.state.legendTarget
|
|
818
|
-
if (id) {
|
|
819
|
-
const found = (this.root as any)?.findById?.(id) as Component | undefined
|
|
820
|
-
if (found) {
|
|
821
|
-
this._legendTarget = found
|
|
822
|
-
;(found as any).on?.('change', this._onLegendChanged, this)
|
|
823
|
-
return found
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
// scene-wide auto-discovery
|
|
828
|
-
const visit = (node: any): Component | undefined => {
|
|
829
|
-
if (!node) return undefined
|
|
830
|
-
if (node.state?.type === 'legend') return node as Component
|
|
831
|
-
const children = node.components as Component[] | undefined
|
|
832
|
-
if (children) {
|
|
833
|
-
for (const c of children) {
|
|
834
|
-
const r = visit(c)
|
|
835
|
-
if (r) return r
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
return undefined
|
|
839
|
-
}
|
|
840
|
-
const found = visit(this.root)
|
|
841
|
-
if (found) {
|
|
842
|
-
this._legendTarget = found
|
|
843
|
-
;(found as any).on?.('change', this._onLegendChanged, this)
|
|
844
|
-
}
|
|
845
|
-
return found
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
private _onLegendChanged = (): void => {
|
|
849
|
-
;(this._realObject as any)?.rebuildStockMesh?.()
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
/**
|
|
853
|
-
* record 의 legend.field 값을 ranges 와 매칭해 색상 해석.
|
|
854
|
-
* - `range.value === recordValue` (카테고리 일치)
|
|
855
|
-
* - `range.min ≤ Number(v) < range.max` (수치 범위)
|
|
856
|
-
* - 매칭 없으면 `defaultColor` 또는 undefined
|
|
857
|
-
*/
|
|
858
|
-
resolveLegendColor(record: any): string | undefined {
|
|
859
|
-
const legend = this.legendTarget
|
|
860
|
-
if (!legend) return undefined
|
|
861
|
-
const status: any = (legend as any).getState?.('status') ?? (legend.state as any)?.status
|
|
862
|
-
if (!status) return undefined
|
|
863
|
-
|
|
864
|
-
const field = status.field as string | undefined
|
|
865
|
-
const ranges = status.ranges as any[] | undefined
|
|
866
|
-
if (!field || !Array.isArray(ranges)) return undefined
|
|
867
|
-
|
|
868
|
-
const value = record?.[field]
|
|
869
|
-
if (value === undefined || value === null) return status.defaultColor
|
|
870
|
-
|
|
871
|
-
for (const range of ranges) {
|
|
872
|
-
if (!range) continue
|
|
873
|
-
if (range.value !== undefined) {
|
|
874
|
-
if (range.value === value) return range.color
|
|
875
|
-
continue
|
|
876
|
-
}
|
|
877
|
-
const num = Number(value)
|
|
878
|
-
if (!Number.isFinite(num)) continue
|
|
879
|
-
const min = range.min !== undefined && range.min !== '' ? Number(range.min) : undefined
|
|
880
|
-
const max = range.max !== undefined && range.max !== '' ? Number(range.max) : undefined
|
|
881
|
-
const minOk = min === undefined || num >= min
|
|
882
|
-
const maxOk = max === undefined || num < max
|
|
883
|
-
if (minOk && maxOk) return range.color
|
|
884
|
-
}
|
|
885
|
-
return status.defaultColor as string | undefined
|
|
886
|
-
}
|
|
811
|
+
// legendTarget / _onLegendChanged / resolveLegendColor — RecordStorage mixin 제공.
|
|
887
812
|
|
|
888
813
|
// ── Click event — rack-cell-click 발사 ────────────────────────────────────
|
|
889
814
|
//
|
|
@@ -951,30 +876,10 @@ export default class Rack
|
|
|
951
876
|
this.trigger('rack-cell-click', payload)
|
|
952
877
|
|
|
953
878
|
// Popup 호출 — 일반 mechanism (Popup 컴포넌트) 활용, anchor 만 클릭된 cell 로 override.
|
|
954
|
-
this._invokePopup(cellId, record)
|
|
879
|
+
if (cellId) this._invokePopup(cellId, record ?? { cellId })
|
|
955
880
|
}
|
|
956
881
|
|
|
957
|
-
|
|
958
|
-
* state.popupRef 가 가리키는 Popup 컴포넌트를 invoke. anchor 를 SlotTarget 으로
|
|
959
|
-
* 지정 — SlotTarget._realObject.object3d 가 cellId 위치의 anchor object3d 를
|
|
960
|
-
* 가리켜 tether / projectToScreen 정확.
|
|
961
|
-
*
|
|
962
|
-
* - popupRef 미설정 → no-op (event 만 발사된 상태로 남음)
|
|
963
|
-
* - 다른 cell 클릭 시 popup 이 새 anchor 로 "이동" (Popup 의 board 등 설정 유지)
|
|
964
|
-
* - frame/empty 영역 클릭 시 호출 안 됨 → popup 그대로 유지
|
|
965
|
-
* - 명시적 close 버튼은 popup 자체의 closable 옵션이 처리
|
|
966
|
-
*/
|
|
967
|
-
private _invokePopup(cellId: string | undefined, record: any): void {
|
|
968
|
-
const popupRefId = this.state.popupRef
|
|
969
|
-
if (!popupRefId || !cellId) return
|
|
970
|
-
const popupComp: any = (this.root as any)?.findById?.(popupRefId)
|
|
971
|
-
if (!popupComp || typeof popupComp.openPopup !== 'function') {
|
|
972
|
-
console.warn(`[storage-rack] popupRef="${popupRefId}" 가 가리키는 컴포넌트 없거나 openPopup 미지원`)
|
|
973
|
-
return
|
|
974
|
-
}
|
|
975
|
-
const anchor = this.slotTargetAt(cellId)
|
|
976
|
-
popupComp.openPopup(record ?? { cellId }, { anchor })
|
|
977
|
-
}
|
|
882
|
+
// _invokePopup — RecordStorage mixin 제공 (cellId=slotId, record=payload).
|
|
978
883
|
|
|
979
884
|
/**
|
|
980
885
|
* 클릭 시 framework 의 mouse NDC (이미 InteractionManager 가 set 한 상태) 를 재사용해
|
|
@@ -1074,6 +979,46 @@ export default class Rack
|
|
|
1074
979
|
return `${bayIdx}-${rowIdx}-${levelIdx}`
|
|
1075
980
|
}
|
|
1076
981
|
|
|
982
|
+
/**
|
|
983
|
+
* Cell-precise approach point — mover 가 *_cell 정면 외부 stand-off_* 까지만.
|
|
984
|
+
*
|
|
985
|
+
* cellId format: `${bay - 1}-${row - 1}-${level - 1}` (0-based).
|
|
986
|
+
*
|
|
987
|
+
* **사용자 의도**: rack 은 *_전면_* 만 포크질 가능. front face = state.top 쪽
|
|
988
|
+
* (작은 z, 3D 의 -Z forward). back face 사용 금지 — forklift 가 rack 후면에
|
|
989
|
+
* 정차하는 동작은 잘못. navigation 의 obstacle avoidance 가 rack 우회 path 계산.
|
|
990
|
+
*
|
|
991
|
+
* forkLen — fork blade tip 부터 mover center 까지 거리. mover 가 그 거리만큼
|
|
992
|
+
* cell face 앞에서 정차 → 정확히 fork blade 가 cell face 정렬.
|
|
993
|
+
*
|
|
994
|
+
* (rotation 지원 V11 — 현재 axis-aligned rack 만, facing default = top.)
|
|
995
|
+
*/
|
|
996
|
+
slotApproachWorldPosition(
|
|
997
|
+
slotId: string,
|
|
998
|
+
_fromPos?: { x: number; y: number; z: number }
|
|
999
|
+
): { x: number; y: number; z: number } {
|
|
1000
|
+
const parts = slotId.split('-')
|
|
1001
|
+
const s: any = this.state
|
|
1002
|
+
const left = s?.left ?? 0
|
|
1003
|
+
const top = s?.top ?? 0
|
|
1004
|
+
const width = s?.width ?? 0
|
|
1005
|
+
|
|
1006
|
+
if (parts.length !== 3) {
|
|
1007
|
+
// fallback — front face center
|
|
1008
|
+
return { x: left + width / 2, y: 0, z: top - (s.depth ?? 100) * 0.5 }
|
|
1009
|
+
}
|
|
1010
|
+
const col = Number(parts[0])
|
|
1011
|
+
const bays = Math.max(1, Math.floor((s.bays as number) ?? 5))
|
|
1012
|
+
|
|
1013
|
+
const cellX = left + (col + 0.5) * (width / bays)
|
|
1014
|
+
|
|
1015
|
+
// fork 진입 거리 — rack.depth 의 절반 (mover 정차 점이 fork blade 가 cell 정면에
|
|
1016
|
+
// 정렬되는 거리). _fromPos 무시 — *_항상 front face_* (사용자 의도: 전면만 포크질).
|
|
1017
|
+
const forkLen = (s.depth ?? 100) * 0.5
|
|
1018
|
+
|
|
1019
|
+
return { x: cellX, y: 0, z: top - forkLen }
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1077
1022
|
// ── 3D ───────────────────────────────────────────────────────────────────
|
|
1078
1023
|
|
|
1079
1024
|
buildRealObject(): RealObject | undefined {
|
package/src/templates/index.ts
CHANGED
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
* - spot — virtual placement marker
|
|
11
11
|
*/
|
|
12
12
|
import spot from './spot.js'
|
|
13
|
+
import stockpile from './stockpile.js'
|
|
14
|
+
import stockpileGrid from './stockpile-grid.js'
|
|
15
|
+
import pickingStation from './picking-station.js'
|
|
13
16
|
const pallet = new URL('../../icons/pallet.png', import.meta.url).href
|
|
14
17
|
const box = new URL('../../icons/box.png', import.meta.url).href
|
|
15
18
|
const parcel = new URL('../../icons/parcel.png', import.meta.url).href
|
|
@@ -196,5 +199,8 @@ export default [
|
|
|
196
199
|
]
|
|
197
200
|
}
|
|
198
201
|
},
|
|
199
|
-
spot
|
|
202
|
+
spot,
|
|
203
|
+
stockpile,
|
|
204
|
+
stockpileGrid,
|
|
205
|
+
pickingStation
|
|
200
206
|
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Reuse parcel.png as a placeholder icon until a dedicated picking-station icon is drawn.
|
|
2
|
+
const icon = new URL('../../icons/parcel.png', import.meta.url).href
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
type: 'picking-station',
|
|
6
|
+
description:
|
|
7
|
+
'work station — carrier stays for processingTimeMs then becomes idle. anchors AMR↔human handoff',
|
|
8
|
+
group: 'storage',
|
|
9
|
+
icon,
|
|
10
|
+
model: {
|
|
11
|
+
type: 'picking-station',
|
|
12
|
+
top: 200,
|
|
13
|
+
left: 400,
|
|
14
|
+
width: 120,
|
|
15
|
+
height: 100,
|
|
16
|
+
depth: 60,
|
|
17
|
+
rotation: 0,
|
|
18
|
+
fillStyle: '#5a8ab8',
|
|
19
|
+
strokeStyle: '#3d6a8f',
|
|
20
|
+
processingTimeMs: 3000,
|
|
21
|
+
status: 'idle'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Reuse parcel.png as a placeholder icon until a dedicated stockpile-grid icon is drawn.
|
|
2
|
+
const icon = new URL('../../icons/parcel.png', import.meta.url).href
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
type: 'stockpile-grid',
|
|
6
|
+
description:
|
|
7
|
+
'grid of block/floor storage cells — each cell is a stockpile with its own records, capacity, and preset override',
|
|
8
|
+
group: 'storage',
|
|
9
|
+
icon,
|
|
10
|
+
model: {
|
|
11
|
+
type: 'stockpile-grid',
|
|
12
|
+
top: 200,
|
|
13
|
+
left: 400,
|
|
14
|
+
width: 300,
|
|
15
|
+
height: 200,
|
|
16
|
+
depth: 5,
|
|
17
|
+
rotation: 0,
|
|
18
|
+
fillStyle: '#c89c5c',
|
|
19
|
+
strokeStyle: '#7a5a2e',
|
|
20
|
+
cols: 3,
|
|
21
|
+
rows: 2,
|
|
22
|
+
cellWidth: 100,
|
|
23
|
+
cellHeight: 100,
|
|
24
|
+
stackPattern: 'row',
|
|
25
|
+
carrierPreset: 'box',
|
|
26
|
+
carrierWidth: 30,
|
|
27
|
+
carrierHeight: 30,
|
|
28
|
+
carrierDepth: 22,
|
|
29
|
+
carrierGap: 10,
|
|
30
|
+
capacity: 20,
|
|
31
|
+
pickPolicy: 'lifo',
|
|
32
|
+
// 데모 — 일부 cell 에 records 미리 채워서 끌어 놓자 마자 적치 보이게.
|
|
33
|
+
data: [
|
|
34
|
+
{ col: 0, row: 0, data: [{ id: 'a1' }, { id: 'a2' }] },
|
|
35
|
+
{ col: 1, row: 0, data: [{ id: 'b1' }, { id: 'b2' }, { id: 'b3' }] },
|
|
36
|
+
{ col: 2, row: 1, data: [{ id: 'c1' }] }
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Reuse parcel.png as a placeholder icon until a dedicated stockpile icon is drawn.
|
|
2
|
+
const icon = new URL('../../icons/parcel.png', import.meta.url).href
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
type: 'stockpile',
|
|
6
|
+
description:
|
|
7
|
+
'block/floor storage — rectangular footprint with auto-stacked virtual carriers (stackPattern × carrierPreset) driven by state.data records',
|
|
8
|
+
group: 'storage' /* line|shape|textAndMedia|chartAndGauge|table|container|dataSource|3D|facility|storage|conveyance|transport|manufacturing|form|etc */,
|
|
9
|
+
icon,
|
|
10
|
+
model: {
|
|
11
|
+
type: 'stockpile',
|
|
12
|
+
top: 200,
|
|
13
|
+
left: 400,
|
|
14
|
+
width: 200,
|
|
15
|
+
height: 150,
|
|
16
|
+
depth: 5,
|
|
17
|
+
rotation: 0,
|
|
18
|
+
fillStyle: '#c89c5c',
|
|
19
|
+
// 바닥 페인트 라인 의도 — fillStyle 보다 진한 색으로 영역 마킹이 눈에 띄게.
|
|
20
|
+
strokeStyle: '#7a5a2e',
|
|
21
|
+
stackPattern: 'row',
|
|
22
|
+
carrierPreset: 'box',
|
|
23
|
+
carrierWidth: 30,
|
|
24
|
+
carrierHeight: 30,
|
|
25
|
+
carrierDepth: 22,
|
|
26
|
+
carrierGap: 10,
|
|
27
|
+
capacity: 30,
|
|
28
|
+
pickPolicy: 'lifo',
|
|
29
|
+
// 데모용 — 새로 끌어다 놓았을 때 적치된 모습이 즉시 보이도록 record 몇 개.
|
|
30
|
+
data: [{ id: 'p-1' }, { id: 'p-2' }, { id: 'p-3' }]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Rack 적재 mover 능력 판정 — rackAcceptsMoverTool 순수 로직 검증.
|
|
3
|
+
*
|
|
4
|
+
* storage-rack.canAcceptFromMover 가 이 함수에 위임. "AMR(agv-deck)은 rack 선반
|
|
5
|
+
* 직접 적재 불가 → 거부" 가 정확히 동작해야 transfer planner 가 in-port 경유를
|
|
6
|
+
* 자동 선택한다 (텔레포트 방지의 핵심).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import 'should'
|
|
10
|
+
import { rackAcceptsMoverTool } from '../src/rack-capability.js'
|
|
11
|
+
|
|
12
|
+
describe('rackAcceptsMoverTool — 적재 mover 능력 게이트', () => {
|
|
13
|
+
it('★ agv-deck (AMR) 거부 — 평탄데크는 선반 직접 적재 불가 ★', () => {
|
|
14
|
+
rackAcceptsMoverTool('agv-deck').should.be.false()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('gripper (crane) 허용', () => {
|
|
18
|
+
rackAcceptsMoverTool('gripper').should.be.true()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('forklift-fork (지게차) 허용', () => {
|
|
22
|
+
rackAcceptsMoverTool('forklift-fork').should.be.true()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('stacker / shuttle (미니로드) 허용', () => {
|
|
26
|
+
rackAcceptsMoverTool('stacker').should.be.true()
|
|
27
|
+
rackAcceptsMoverTool('shuttle').should.be.true()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('toolType 없음 (능력 미상) → 허용 (보수적 통과)', () => {
|
|
31
|
+
rackAcceptsMoverTool(undefined).should.be.true()
|
|
32
|
+
rackAcceptsMoverTool(null).should.be.true()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('blockedTools override — 빈 배열이면 모두 허용', () => {
|
|
36
|
+
rackAcceptsMoverTool('agv-deck', []).should.be.true()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('blockedTools 명시 — gripper 도 거부 가능', () => {
|
|
40
|
+
rackAcceptsMoverTool('gripper', ['gripper', 'agv-deck']).should.be.false()
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('blockedTools 가 배열 아님 (잘못된 값) → 허용', () => {
|
|
44
|
+
rackAcceptsMoverTool('agv-deck', 'invalid' as any).should.be.true()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('default blockedTools 는 agv-deck 만 — 다른 평탄류 영향 없음', () => {
|
|
48
|
+
// 'agv' 같은 변형은 default 에 없으므로 허용 (사용자가 명시해야 거부)
|
|
49
|
+
rackAcceptsMoverTool('agv').should.be.true()
|
|
50
|
+
})
|
|
51
|
+
})
|
package/translations/en.json
CHANGED
|
@@ -10,12 +10,10 @@
|
|
|
10
10
|
"component.mobile-storage-rack": "mobile storage rack",
|
|
11
11
|
"component.spot": "spot",
|
|
12
12
|
"component.generic-container": "container",
|
|
13
|
-
|
|
14
13
|
"label.tracking-id": "tracking id",
|
|
15
14
|
"label.material": "material",
|
|
16
15
|
"label.depth": "depth",
|
|
17
16
|
"label.fill": "fill",
|
|
18
|
-
|
|
19
17
|
"label.bays": "bays",
|
|
20
18
|
"label.levels": "levels",
|
|
21
19
|
"label.rows": "rows",
|
|
@@ -39,8 +37,7 @@
|
|
|
39
37
|
"label.stock-scale": "stock scale",
|
|
40
38
|
"label.legend-target": "legend target",
|
|
41
39
|
"label.shelf-base-height": "shelf base height",
|
|
42
|
-
|
|
43
|
-
"label.status": "status",
|
|
40
|
+
"label.status": "Status",
|
|
44
41
|
"label.simulate": "simulate",
|
|
45
42
|
"label.speed": "speed",
|
|
46
43
|
"label.bound-holders": "bound holders",
|
|
@@ -51,7 +48,6 @@
|
|
|
51
48
|
"label.fork-extension": "fork extension",
|
|
52
49
|
"label.fork-lift": "fork lift",
|
|
53
50
|
"label.pocket-depth": "pocket depth",
|
|
54
|
-
|
|
55
51
|
"option.idle": "idle",
|
|
56
52
|
"option.moving": "moving",
|
|
57
53
|
"option.loading": "loading",
|
|
@@ -64,5 +60,26 @@
|
|
|
64
60
|
"option.multi": "multi",
|
|
65
61
|
"option.bulk": "bulk",
|
|
66
62
|
"option.wood": "wood",
|
|
67
|
-
"option.plastic": "plastic"
|
|
63
|
+
"option.plastic": "plastic",
|
|
64
|
+
"component.stockpile": "Stockpile",
|
|
65
|
+
"component.stockpile-grid": "Stockpile Grid",
|
|
66
|
+
"label.stack-pattern": "Stack Pattern",
|
|
67
|
+
"label.carrier-preset": "Carrier Preset",
|
|
68
|
+
"label.carrier-width": "Carrier Width",
|
|
69
|
+
"label.carrier-height": "Carrier Height",
|
|
70
|
+
"label.carrier-depth": "Carrier Depth",
|
|
71
|
+
"label.carrier-gap": "Carrier Gap",
|
|
72
|
+
"label.capacity": "Capacity",
|
|
73
|
+
"label.stack-height-limit": "Stack Height Limit",
|
|
74
|
+
"label.pick-policy": "Pick Policy",
|
|
75
|
+
"label.cols": "Columns",
|
|
76
|
+
"label.cell-width": "Cell Width",
|
|
77
|
+
"label.cell-height": "Cell Height",
|
|
78
|
+
"component.picking-station": "Picking Station",
|
|
79
|
+
"label.processing-time-ms": "Processing Time (ms)",
|
|
80
|
+
"component.container": "container",
|
|
81
|
+
"component.wood pallet": "wood pallet",
|
|
82
|
+
"component.plastic pallet": "plastic pallet",
|
|
83
|
+
"component.wood box": "wood box",
|
|
84
|
+
"component.plastic box": "plastic box"
|
|
68
85
|
}
|
package/translations/ja.json
CHANGED
|
@@ -10,12 +10,10 @@
|
|
|
10
10
|
"component.mobile-storage-rack": "移動式ラック",
|
|
11
11
|
"component.spot": "スポット",
|
|
12
12
|
"component.generic-container": "コンテナ",
|
|
13
|
-
|
|
14
13
|
"label.tracking-id": "追跡ID",
|
|
15
14
|
"label.material": "材質",
|
|
16
15
|
"label.depth": "深さ",
|
|
17
16
|
"label.fill": "塗りつぶし",
|
|
18
|
-
|
|
19
17
|
"label.bays": "ベイ数",
|
|
20
18
|
"label.levels": "段数",
|
|
21
19
|
"label.rows": "行",
|
|
@@ -39,8 +37,7 @@
|
|
|
39
37
|
"label.stock-scale": "在庫スケール",
|
|
40
38
|
"label.legend-target": "凡例対象",
|
|
41
39
|
"label.shelf-base-height": "棚の基準高さ",
|
|
42
|
-
|
|
43
|
-
"label.status": "状態",
|
|
40
|
+
"label.status": "ステータス",
|
|
44
41
|
"label.simulate": "シミュレーション",
|
|
45
42
|
"label.speed": "速度",
|
|
46
43
|
"label.bound-holders": "バインドホルダー",
|
|
@@ -51,7 +48,6 @@
|
|
|
51
48
|
"label.fork-extension": "フォーク伸縮",
|
|
52
49
|
"label.fork-lift": "フォーク昇降",
|
|
53
50
|
"label.pocket-depth": "ポケットの深さ",
|
|
54
|
-
|
|
55
51
|
"option.idle": "待機",
|
|
56
52
|
"option.moving": "移動中",
|
|
57
53
|
"option.loading": "積込中",
|
|
@@ -64,5 +60,26 @@
|
|
|
64
60
|
"option.multi": "マルチ",
|
|
65
61
|
"option.bulk": "バルク",
|
|
66
62
|
"option.wood": "木製",
|
|
67
|
-
"option.plastic": "プラスチック"
|
|
63
|
+
"option.plastic": "プラスチック",
|
|
64
|
+
"component.stockpile": "平地保管",
|
|
65
|
+
"component.stockpile-grid": "平地保管グリッド",
|
|
66
|
+
"label.stack-pattern": "積み方",
|
|
67
|
+
"label.carrier-preset": "キャリア種類",
|
|
68
|
+
"label.carrier-width": "キャリア幅",
|
|
69
|
+
"label.carrier-height": "キャリア高さ",
|
|
70
|
+
"label.carrier-depth": "キャリア奥行",
|
|
71
|
+
"label.carrier-gap": "キャリア間隔",
|
|
72
|
+
"label.capacity": "収容量",
|
|
73
|
+
"label.stack-height-limit": "段数制限",
|
|
74
|
+
"label.pick-policy": "取出方針",
|
|
75
|
+
"label.cols": "列数",
|
|
76
|
+
"label.cell-width": "セル幅",
|
|
77
|
+
"label.cell-height": "セル高さ",
|
|
78
|
+
"component.picking-station": "ピッキングステーション",
|
|
79
|
+
"label.processing-time-ms": "処理時間 (ms)",
|
|
80
|
+
"component.container": "コンテナ",
|
|
81
|
+
"component.wood pallet": "木製パレット",
|
|
82
|
+
"component.plastic pallet": "プラスチックパレット",
|
|
83
|
+
"component.wood box": "木製ボックス",
|
|
84
|
+
"component.plastic box": "プラスチックボックス"
|
|
68
85
|
}
|
package/translations/ko.json
CHANGED
|
@@ -10,12 +10,10 @@
|
|
|
10
10
|
"component.mobile-storage-rack": "이동형 저장 랙",
|
|
11
11
|
"component.spot": "스팟",
|
|
12
12
|
"component.generic-container": "컨테이너",
|
|
13
|
-
|
|
14
13
|
"label.tracking-id": "추적 ID",
|
|
15
14
|
"label.material": "재질",
|
|
16
15
|
"label.depth": "깊이",
|
|
17
16
|
"label.fill": "채움",
|
|
18
|
-
|
|
19
17
|
"label.bays": "베이 수",
|
|
20
18
|
"label.levels": "단수",
|
|
21
19
|
"label.rows": "행",
|
|
@@ -39,7 +37,6 @@
|
|
|
39
37
|
"label.stock-scale": "재고 크기",
|
|
40
38
|
"label.legend-target": "범례 대상",
|
|
41
39
|
"label.shelf-base-height": "선반 시작 높이",
|
|
42
|
-
|
|
43
40
|
"label.status": "상태",
|
|
44
41
|
"label.simulate": "시뮬레이션",
|
|
45
42
|
"label.speed": "속도",
|
|
@@ -51,7 +48,6 @@
|
|
|
51
48
|
"label.fork-extension": "포크 신축",
|
|
52
49
|
"label.fork-lift": "포크 들기",
|
|
53
50
|
"label.pocket-depth": "포켓 깊이",
|
|
54
|
-
|
|
55
51
|
"option.idle": "대기",
|
|
56
52
|
"option.moving": "이동중",
|
|
57
53
|
"option.loading": "적재중",
|
|
@@ -64,5 +60,26 @@
|
|
|
64
60
|
"option.multi": "다중",
|
|
65
61
|
"option.bulk": "벌크",
|
|
66
62
|
"option.wood": "나무",
|
|
67
|
-
"option.plastic": "플라스틱"
|
|
63
|
+
"option.plastic": "플라스틱",
|
|
64
|
+
"component.stockpile": "평치",
|
|
65
|
+
"component.stockpile-grid": "평치 그리드",
|
|
66
|
+
"label.stack-pattern": "적치 패턴",
|
|
67
|
+
"label.carrier-preset": "캐리어 종류",
|
|
68
|
+
"label.carrier-width": "캐리어 너비",
|
|
69
|
+
"label.carrier-height": "캐리어 높이",
|
|
70
|
+
"label.carrier-depth": "캐리어 깊이",
|
|
71
|
+
"label.carrier-gap": "캐리어 간격",
|
|
72
|
+
"label.capacity": "수용량",
|
|
73
|
+
"label.stack-height-limit": "적치 단수 제한",
|
|
74
|
+
"label.pick-policy": "출고 정책",
|
|
75
|
+
"label.cols": "열 수",
|
|
76
|
+
"label.cell-width": "셀 너비",
|
|
77
|
+
"label.cell-height": "셀 높이",
|
|
78
|
+
"component.picking-station": "피킹 스테이션",
|
|
79
|
+
"label.processing-time-ms": "처리 시간 (ms)",
|
|
80
|
+
"component.container": "컨테이너",
|
|
81
|
+
"component.wood pallet": "목재 팔레트",
|
|
82
|
+
"component.plastic pallet": "플라스틱 팔레트",
|
|
83
|
+
"component.wood box": "목재 박스",
|
|
84
|
+
"component.plastic box": "플라스틱 박스"
|
|
68
85
|
}
|