@fmsim/board 2.0.0-beta.81 → 2.0.0-beta.82
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/custom-elements.json +1030 -739
- package/dist/src/layers/mcs-mouse-event-handler.js +36 -26
- package/dist/src/layers/mcs-mouse-event-handler.js.map +1 -1
- package/dist/src/ox-board-viewer.d.ts +48 -0
- package/dist/src/ox-board-viewer.js +278 -135
- package/dist/src/ox-board-viewer.js.map +1 -1
- package/dist/src/playback/carrier-tracking-store.d.ts +51 -0
- package/dist/src/playback/carrier-tracking-store.js +115 -0
- package/dist/src/playback/carrier-tracking-store.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/icons/fab/mode-2d.svg +25 -0
- package/icons/fab/mode-3d.svg +25 -0
- package/icons/fab/playback-play.svg +49 -0
- package/icons/fab/playback-stop.svg +50 -0
- package/package.json +3 -3
|
@@ -1,38 +1,48 @@
|
|
|
1
1
|
import { EventMap } from '@hatiolab/things-scene';
|
|
2
2
|
function dispatchFetchLvModelDataByMouseEvent(type, event, target) {
|
|
3
|
+
var _a, _b, _c;
|
|
3
4
|
const parent = target.parentObject;
|
|
5
|
+
// 플레이백 모드에서는 외부 백엔드 fetch 트리거 (fetchLvModelDataByMouseEvent
|
|
6
|
+
// window 이벤트) 를 차단. 사용자가 보는 시점은 과거이므로 현재 시점 fetch 가
|
|
7
|
+
// 의미 정합성을 깸. realTarget 결정 (parent escalation) 은 그대로 — popup /
|
|
8
|
+
// highlight 등 클릭 후속 동작에는 영향 없음.
|
|
9
|
+
const playbackMode = !!((_c = (_b = (_a = target === null || target === void 0 ? void 0 : target.root) === null || _a === void 0 ? void 0 : _a.app) === null || _b === void 0 ? void 0 : _b.scene) === null || _c === void 0 ? void 0 : _c.playbackMode);
|
|
4
10
|
if (target.getState('id')) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
if (!playbackMode) {
|
|
12
|
+
window.dispatchEvent(new CustomEvent('fetchLvModelDataByMouseEvent', {
|
|
13
|
+
detail: {
|
|
14
|
+
value: {
|
|
15
|
+
type,
|
|
16
|
+
event,
|
|
17
|
+
model: {
|
|
18
|
+
...target.state,
|
|
19
|
+
parentObject: parent
|
|
20
|
+
? {
|
|
21
|
+
id: parent.get('id'),
|
|
22
|
+
type: parent.get('type')
|
|
23
|
+
}
|
|
24
|
+
: null
|
|
25
|
+
},
|
|
26
|
+
data: target.data
|
|
27
|
+
}
|
|
20
28
|
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
23
31
|
return parent || target;
|
|
24
32
|
}
|
|
25
33
|
else if (parent && parent.getState('id')) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
if (!playbackMode) {
|
|
35
|
+
window.dispatchEvent(new CustomEvent('fetchLvModelDataByMouseEvent', {
|
|
36
|
+
detail: {
|
|
37
|
+
value: {
|
|
38
|
+
type,
|
|
39
|
+
event,
|
|
40
|
+
model: parent.state,
|
|
41
|
+
data: parent.data
|
|
42
|
+
}
|
|
33
43
|
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
36
46
|
return parent;
|
|
37
47
|
}
|
|
38
48
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcs-mouse-event-handler.js","sourceRoot":"","sources":["../../../src/layers/mcs-mouse-event-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAE5D,SAAS,oCAAoC,CAC3C,IAAoC,EACpC,KAAiB,EACjB,MAAgD
|
|
1
|
+
{"version":3,"file":"mcs-mouse-event-handler.js","sourceRoot":"","sources":["../../../src/layers/mcs-mouse-event-handler.ts"],"names":[],"mappings":"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","sourcesContent":["import { Component, EventMap } from '@hatiolab/things-scene'\n\nfunction dispatchFetchLvModelDataByMouseEvent(\n type: 'hover' | 'click' | 'dblclick',\n event: MouseEvent,\n target: Component & { parentObject?: Component }\n): Component | undefined {\n const parent = target.parentObject\n\n // 플레이백 모드에서는 외부 백엔드 fetch 트리거 (fetchLvModelDataByMouseEvent\n // window 이벤트) 를 차단. 사용자가 보는 시점은 과거이므로 현재 시점 fetch 가\n // 의미 정합성을 깸. realTarget 결정 (parent escalation) 은 그대로 — popup /\n // highlight 등 클릭 후속 동작에는 영향 없음.\n const playbackMode = !!(target as any)?.root?.app?.scene?.playbackMode\n\n if (target.getState('id')) {\n if (!playbackMode) {\n window.dispatchEvent(\n new CustomEvent('fetchLvModelDataByMouseEvent', {\n detail: {\n value: {\n type,\n event,\n model: {\n ...target.state,\n parentObject: parent\n ? {\n id: parent.get('id'),\n type: parent.get('type')\n }\n : null\n },\n data: target.data\n }\n }\n })\n )\n }\n\n return parent || target\n } else if (parent && parent.getState('id')) {\n if (!playbackMode) {\n window.dispatchEvent(\n new CustomEvent('fetchLvModelDataByMouseEvent', {\n detail: {\n value: {\n type,\n event,\n model: parent.state,\n data: parent.data\n }\n }\n })\n )\n }\n\n return parent\n }\n}\n\n// function onmouseenter(event: MouseEvent, hint: any) {\n// const target = hint.origin\n\n// event.stopPropagation()\n\n// dispatchFetchLvModelDataByMouseEvent('hover', event, target)\n// }\n\nfunction onclick(event: MouseEvent, hint: any) {\n const target = hint.origin\n\n event.stopPropagation()\n\n setTimeout(() => {\n if (target.currentEvent !== 'dblclick') {\n const realTarget = dispatchFetchLvModelDataByMouseEvent('click', event, target)\n\n window.dispatchEvent(\n new CustomEvent('clickComponent', {\n detail: realTarget || target\n })\n )\n }\n }, 300)\n}\n\nfunction ondblclick(event: MouseEvent, hint: any) {\n const target = hint.origin\n\n setTimeout(() => {\n target.currentEvent = ''\n }, 500)\n\n event.stopPropagation()\n\n target.currentEvent = 'dblclick'\n\n dispatchFetchLvModelDataByMouseEvent('dblclick', event, target)\n}\n\nEventMap.register('mcs-event-handler', {\n 'model-layer': {\n '(all)': {\n // mouseenter: onmouseenter, /* 사용되지 않고 있음 */\n click: onclick,\n dblclick: ondblclick\n }\n }\n})\n"]}
|
|
@@ -75,6 +75,37 @@ export declare class BoardViewer extends LitElement {
|
|
|
75
75
|
showScene(boardId: string, bindingData?: any): Promise<void>;
|
|
76
76
|
private resizeHandler;
|
|
77
77
|
bindSceneEvents(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Tracker fallback resolver — 플레이백 시각의 carrier 위치를 컴포넌트로 변환.
|
|
80
|
+
* NA / 정보 없음 / 머신 미존재 → null (Tracker 가 highlight 없이 추적만 유지).
|
|
81
|
+
*
|
|
82
|
+
* 매 1초 polling 마다 호출되며, 결과 target 이 직전과 동일하면 로그 skip.
|
|
83
|
+
*/
|
|
84
|
+
private _resolveCarrierFallback;
|
|
85
|
+
/**
|
|
86
|
+
* deco-arrow 라이프사이클 — 이전 target 끄고 새 target 에 bouncing arrow 부착.
|
|
87
|
+
* 둘 다 null 가능 (해제만, 부착만).
|
|
88
|
+
*/
|
|
89
|
+
private _setFallbackArrow;
|
|
90
|
+
/**
|
|
91
|
+
* 1초 polling — 화살표 부착 대상을 결정.
|
|
92
|
+
* 1) 플레이백 종료 / 추적 ID 없음 → 화살표 해제
|
|
93
|
+
* 2) carrier 가 scene 에 직접 존재 → **carrier 자체에 화살표 부착**
|
|
94
|
+
* (things-scene Tracker 가 carrier 발견 시 fallback 콜백 호출을 멈춰
|
|
95
|
+
* resolver 경로로는 우리가 모름. monitor 에서 직접 책임)
|
|
96
|
+
* 3) carrier 없음 + resolver 가 3초 이상 호출 안 됨 → 화살표 해제
|
|
97
|
+
* (그 외엔 resolver 의 logIfChanged 가 fallback target 으로 부착 중)
|
|
98
|
+
*/
|
|
99
|
+
private _startFallbackArrowMonitor;
|
|
100
|
+
private _stopFallbackArrowMonitor;
|
|
101
|
+
/** 부착된 deco-arrow 강제 해제 + dedupe 상태 초기화. */
|
|
102
|
+
private _clearFallbackArrow;
|
|
103
|
+
/**
|
|
104
|
+
* onSearchSubmit hook — 영구추적 carrierName 이 설정되는 순간.
|
|
105
|
+
* 플레이백 윈도우가 잡혀 있으면 즉시 store 충전. 윈도우가 없으면
|
|
106
|
+
* 플레이백 시작 시 (_onPlaybackStart) 에 충전.
|
|
107
|
+
*/
|
|
108
|
+
private _onCarrierTrackingSubmit;
|
|
78
109
|
unbindSceneEvents(scene: any): void;
|
|
79
110
|
transientShowButtons(stop?: boolean): void;
|
|
80
111
|
onTapNext(): void;
|
|
@@ -83,6 +114,23 @@ export declare class BoardViewer extends LitElement {
|
|
|
83
114
|
onTapToggle3D(): void;
|
|
84
115
|
private _dataSubscriptionProvider;
|
|
85
116
|
private _playbackEngine?;
|
|
117
|
+
/**
|
|
118
|
+
* 영구추적 fallback 데이터 캐시 — 플레이백 윈도우 × carrierName 단위로
|
|
119
|
+
* carrier 위치 이력을 보관. carrier 가 scene 에 직접 안 보일 때
|
|
120
|
+
* Tracker fallback 콜백이 lookupAt 으로 머신/유닛을 찾아 반환.
|
|
121
|
+
*/
|
|
122
|
+
private _carrierTrackingStore;
|
|
123
|
+
/** 마지막으로 시작한 플레이백 윈도우 (carrier store load 트리거용). */
|
|
124
|
+
private _playbackWindow?;
|
|
125
|
+
/**
|
|
126
|
+
* fallback 결과(target 컴포넌트) dedupe — 직전과 동일한 target 일 땐 로그 skip
|
|
127
|
+
* + deco-arrow 라이프사이클 (target 변경 시 이전 끄고 새로 부착).
|
|
128
|
+
*/
|
|
129
|
+
private _lastFallbackTarget;
|
|
130
|
+
/** _resolveCarrierFallback 마지막 호출 timestamp — carrier 발견 감지용. */
|
|
131
|
+
private _lastFallbackResolvedAt;
|
|
132
|
+
/** carrier 발견 감지 / deco-arrow 자동 해제용 1초 polling 타이머. */
|
|
133
|
+
private _fallbackArrowMonitor?;
|
|
86
134
|
onTapPlayback(): void;
|
|
87
135
|
private _onPlaybackStart;
|
|
88
136
|
private _onPlaybackPause;
|
|
@@ -9,11 +9,18 @@ import './ox-playback-controls.js';
|
|
|
9
9
|
import { css, html, LitElement, nothing } from 'lit';
|
|
10
10
|
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
11
11
|
import { PlaybackEngine } from './playback/playback-engine.js';
|
|
12
|
+
import { CarrierTrackingStore } from './playback/carrier-tracking-store.js';
|
|
12
13
|
import { create, LoadTracker, LoadingOverlay, SCENE_MODE } from '@hatiolab/things-scene';
|
|
13
14
|
import { isIOS, fullscreen, exitfullscreen, togglefullscreen } from '@operato/utils';
|
|
14
15
|
import { ScrollbarStyles } from '@operato/styles';
|
|
15
16
|
import { DataStorage } from './data-storage/data-storage.js';
|
|
16
17
|
import { runScenario, startScenario, DataSubscriptionProviderImpl, isRestfulAvailable } from '@fmsim/api';
|
|
18
|
+
// FAB 아이콘 — packages/board/icons/fab/ 의 외부 SVG asset 로 분리.
|
|
19
|
+
// 기존엔 inline SVG 였으나 (a) 가독성 향상, (b) 디자이너 단독 수정, (c) 캐시 활용 목적.
|
|
20
|
+
const ICON_FAB_PLAYBACK_PLAY = new URL('../../icons/fab/playback-play.svg', import.meta.url).href;
|
|
21
|
+
const ICON_FAB_PLAYBACK_STOP = new URL('../../icons/fab/playback-stop.svg', import.meta.url).href;
|
|
22
|
+
const ICON_FAB_MODE_2D = new URL('../../icons/fab/mode-2d.svg', import.meta.url).href;
|
|
23
|
+
const ICON_FAB_MODE_3D = new URL('../../icons/fab/mode-3d.svg', import.meta.url).href;
|
|
17
24
|
let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
18
25
|
constructor() {
|
|
19
26
|
var _a;
|
|
@@ -198,6 +205,19 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
198
205
|
};
|
|
199
206
|
// ── Data Subscription ──
|
|
200
207
|
this._dataSubscriptionProvider = isRestfulAvailable() ? undefined : new DataSubscriptionProviderImpl();
|
|
208
|
+
/**
|
|
209
|
+
* 영구추적 fallback 데이터 캐시 — 플레이백 윈도우 × carrierName 단위로
|
|
210
|
+
* carrier 위치 이력을 보관. carrier 가 scene 에 직접 안 보일 때
|
|
211
|
+
* Tracker fallback 콜백이 lookupAt 으로 머신/유닛을 찾아 반환.
|
|
212
|
+
*/
|
|
213
|
+
this._carrierTrackingStore = new CarrierTrackingStore();
|
|
214
|
+
/**
|
|
215
|
+
* fallback 결과(target 컴포넌트) dedupe — 직전과 동일한 target 일 땐 로그 skip
|
|
216
|
+
* + deco-arrow 라이프사이클 (target 변경 시 이전 끄고 새로 부착).
|
|
217
|
+
*/
|
|
218
|
+
this._lastFallbackTarget = null;
|
|
219
|
+
/** _resolveCarrierFallback 마지막 호출 timestamp — carrier 발견 감지용. */
|
|
220
|
+
this._lastFallbackResolvedAt = 0;
|
|
201
221
|
}
|
|
202
222
|
get _is3dMode() {
|
|
203
223
|
var _a, _b, _c;
|
|
@@ -231,139 +251,21 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
231
251
|
class="sub-fab"
|
|
232
252
|
@click=${() => { this._fabExpanded = false; this.onTapPlayback(); }}
|
|
233
253
|
title=${this._playbackActive ? 'stop playback' : 'start playback'}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
d="M 52,22 A 28,28 0 1,1 30,40"
|
|
240
|
-
fill="none"
|
|
241
|
-
stroke="#F87171"
|
|
242
|
-
stroke-width="5"
|
|
243
|
-
stroke-linecap="round"
|
|
244
|
-
/>
|
|
245
|
-
<path d="M 52,12 L 37,24 L 55,33 Z" fill="#F87171" />
|
|
246
|
-
</g>
|
|
247
|
-
<rect
|
|
248
|
-
x="36"
|
|
249
|
-
y="36"
|
|
250
|
-
width="28"
|
|
251
|
-
height="28"
|
|
252
|
-
rx="3"
|
|
253
|
-
fill="#F87171"
|
|
254
|
-
stroke="#F87171"
|
|
255
|
-
stroke-width="3"
|
|
256
|
-
stroke-linejoin="round"
|
|
257
|
-
/>
|
|
258
|
-
<text
|
|
259
|
-
x="50"
|
|
260
|
-
y="53.5"
|
|
261
|
-
font-family="Verdana, sans-serif"
|
|
262
|
-
font-size="9"
|
|
263
|
-
font-weight="900"
|
|
264
|
-
fill="#ffffff"
|
|
265
|
-
text-anchor="middle"
|
|
266
|
-
>OFF</text>
|
|
267
|
-
</svg>`
|
|
268
|
-
: html `<svg slot="icon" width="48" height="48" viewBox="0 0 100 100" style="display: block;">
|
|
269
|
-
<circle cx="50" cy="50" r="50" fill="#F9EBEA" />
|
|
270
|
-
<g transform="translate(-5, 0)">
|
|
271
|
-
<path
|
|
272
|
-
d="M 52,22 A 28,28 0 1,1 30,40"
|
|
273
|
-
fill="none"
|
|
274
|
-
stroke="#F87171"
|
|
275
|
-
stroke-width="5"
|
|
276
|
-
stroke-linecap="round"
|
|
277
|
-
/>
|
|
278
|
-
<path d="M 52,12 L 37,24 L 55,33 Z" fill="#F87171" />
|
|
279
|
-
</g>
|
|
280
|
-
<path
|
|
281
|
-
d="M 42,35 L 65,50 L 42,65 Z"
|
|
282
|
-
fill="#F87171"
|
|
283
|
-
stroke="#F87171"
|
|
284
|
-
stroke-width="3"
|
|
285
|
-
stroke-linejoin="round"
|
|
286
|
-
/>
|
|
287
|
-
<text
|
|
288
|
-
x="49.5"
|
|
289
|
-
y="53"
|
|
290
|
-
font-family="Verdana, sans-serif"
|
|
291
|
-
font-size="9"
|
|
292
|
-
font-weight="900"
|
|
293
|
-
fill="#ffffff"
|
|
294
|
-
text-anchor="middle"
|
|
295
|
-
>RE:</text>
|
|
296
|
-
</svg>`}</md-fab>
|
|
254
|
+
><span slot="icon" class="fab-icon-wrap"><img
|
|
255
|
+
class="fab-icon"
|
|
256
|
+
src=${this._playbackActive ? ICON_FAB_PLAYBACK_STOP : ICON_FAB_PLAYBACK_PLAY}
|
|
257
|
+
alt=${this._playbackActive ? 'stop playback' : 'start playback'}
|
|
258
|
+
/></span></md-fab>
|
|
297
259
|
|
|
298
260
|
<md-fab
|
|
299
261
|
class="sub-fab"
|
|
300
262
|
@click=${() => { this._fabExpanded = false; this.onTapToggle3D(); }}
|
|
301
263
|
title=${this._is3dMode ? '2D mode' : '3D mode'}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
d="M 72,32 A 30,30 0 0,0 38,28"
|
|
308
|
-
fill="none"
|
|
309
|
-
stroke="#F87171"
|
|
310
|
-
stroke-width="5"
|
|
311
|
-
stroke-linecap="round"
|
|
312
|
-
/>
|
|
313
|
-
<path d="M 38,20 L 22,30 L 38,40 Z" fill="#F87171" />
|
|
314
|
-
</g>
|
|
315
|
-
<g transform="rotate(235, 50, 50)">
|
|
316
|
-
<path
|
|
317
|
-
d="M 72,32 A 30,30 0 0,0 38,28"
|
|
318
|
-
fill="none"
|
|
319
|
-
stroke="#F87171"
|
|
320
|
-
stroke-width="5"
|
|
321
|
-
stroke-linecap="round"
|
|
322
|
-
/>
|
|
323
|
-
<path d="M 38,20 L 22,30 L 38,40 Z" fill="#F87171" />
|
|
324
|
-
</g>
|
|
325
|
-
<text
|
|
326
|
-
x="50"
|
|
327
|
-
y="57.5"
|
|
328
|
-
font-family="Verdana, sans-serif"
|
|
329
|
-
font-size="20"
|
|
330
|
-
font-weight="900"
|
|
331
|
-
fill="#F87171"
|
|
332
|
-
text-anchor="middle"
|
|
333
|
-
>2D</text>
|
|
334
|
-
</svg>`
|
|
335
|
-
: html `<svg slot="icon" width="48" height="48" viewBox="0 0 100 100" style="display: block;">
|
|
336
|
-
<circle cx="50" cy="50" r="50" fill="#F9EBEA" />
|
|
337
|
-
<g transform="rotate(55, 50, 50)">
|
|
338
|
-
<path
|
|
339
|
-
d="M 72,32 A 30,30 0 0,0 38,28"
|
|
340
|
-
fill="none"
|
|
341
|
-
stroke="#F87171"
|
|
342
|
-
stroke-width="5"
|
|
343
|
-
stroke-linecap="round"
|
|
344
|
-
/>
|
|
345
|
-
<path d="M 38,20 L 22,30 L 38,40 Z" fill="#F87171" />
|
|
346
|
-
</g>
|
|
347
|
-
<g transform="rotate(235, 50, 50)">
|
|
348
|
-
<path
|
|
349
|
-
d="M 72,32 A 30,30 0 0,0 38,28"
|
|
350
|
-
fill="none"
|
|
351
|
-
stroke="#F87171"
|
|
352
|
-
stroke-width="5"
|
|
353
|
-
stroke-linecap="round"
|
|
354
|
-
/>
|
|
355
|
-
<path d="M 38,20 L 22,30 L 38,40 Z" fill="#F87171" />
|
|
356
|
-
</g>
|
|
357
|
-
<text
|
|
358
|
-
x="50"
|
|
359
|
-
y="57.5"
|
|
360
|
-
font-family="Verdana, sans-serif"
|
|
361
|
-
font-size="20"
|
|
362
|
-
font-weight="900"
|
|
363
|
-
fill="#F87171"
|
|
364
|
-
text-anchor="middle"
|
|
365
|
-
>3D</text>
|
|
366
|
-
</svg>`}</md-fab>
|
|
264
|
+
><span slot="icon" class="fab-icon-wrap"><img
|
|
265
|
+
class="fab-icon"
|
|
266
|
+
src=${this._is3dMode ? ICON_FAB_MODE_2D : ICON_FAB_MODE_3D}
|
|
267
|
+
alt=${this._is3dMode ? '2D mode' : '3D mode'}
|
|
268
|
+
/></span></md-fab>
|
|
367
269
|
</div>
|
|
368
270
|
`;
|
|
369
271
|
var prev = !this.hideNavigation
|
|
@@ -472,6 +374,10 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
472
374
|
(_e = this._dataSubscriptionProvider) === null || _e === void 0 ? void 0 : _e.resume();
|
|
473
375
|
this._playbackActive = false;
|
|
474
376
|
}
|
|
377
|
+
this._playbackWindow = undefined;
|
|
378
|
+
this._carrierTrackingStore.clear();
|
|
379
|
+
this._clearFallbackArrow();
|
|
380
|
+
this._stopFallbackArrowMonitor();
|
|
475
381
|
if (this._playbackEngine) {
|
|
476
382
|
this._playbackEngine.dispose();
|
|
477
383
|
this._playbackEngine = undefined;
|
|
@@ -635,7 +541,203 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
635
541
|
// VIEW 모드 SearchOverlay 디폴트 = 영구 추적 (things-scene 자동 처리).
|
|
636
542
|
// application 은 trackerOptions (color/fallback 등) 또는 onSearchSubmit/onSearchClose
|
|
637
543
|
// hook (디폴트 override) 만 제공.
|
|
638
|
-
|
|
544
|
+
//
|
|
545
|
+
// fallback: 원본 carrier 컴포넌트가 scene 에 없을 때 호출 (Tracker 1s polling).
|
|
546
|
+
// 플레이백 중이면 carrier-location history 에서 현재 시각의 머신/유닛을 lookup 해
|
|
547
|
+
// 해당 컴포넌트를 highlight 대상으로 반환. 플레이백 비활성 / 시간 정보 없음 /
|
|
548
|
+
// NA / 보드에 머신 없음 → null 반환해서 highlight 없이 추적만 유지.
|
|
549
|
+
this._scene.trackerOptions = {
|
|
550
|
+
color: BoardViewer_1.TRACKING_COLOR,
|
|
551
|
+
fallback: (_id, scene) => this._resolveCarrierFallback(scene)
|
|
552
|
+
};
|
|
553
|
+
// SearchOverlay submit/close hook — carrier-location store 의 충전/정리 트리거.
|
|
554
|
+
// 디폴트 동작 (tracker.start/stop) 은 그대로 위임 (return undefined).
|
|
555
|
+
this._scene.onSearchSubmit = (detail) => {
|
|
556
|
+
this._onCarrierTrackingSubmit(detail === null || detail === void 0 ? void 0 : detail.query);
|
|
557
|
+
};
|
|
558
|
+
this._scene.onSearchClose = () => {
|
|
559
|
+
this._carrierTrackingStore.clear();
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Tracker fallback resolver — 플레이백 시각의 carrier 위치를 컴포넌트로 변환.
|
|
564
|
+
* NA / 정보 없음 / 머신 미존재 → null (Tracker 가 highlight 없이 추적만 유지).
|
|
565
|
+
*
|
|
566
|
+
* 매 1초 polling 마다 호출되며, 결과 target 이 직전과 동일하면 로그 skip.
|
|
567
|
+
*/
|
|
568
|
+
_resolveCarrierFallback(scene) {
|
|
569
|
+
var _a, _b, _c, _d;
|
|
570
|
+
if (!this._playbackActive)
|
|
571
|
+
return null;
|
|
572
|
+
const currentTime = (_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.currentTime;
|
|
573
|
+
if (!currentTime)
|
|
574
|
+
return null;
|
|
575
|
+
const timeMs = Date.parse(currentTime.replace(' ', 'T'));
|
|
576
|
+
if (!Number.isFinite(timeMs))
|
|
577
|
+
return null;
|
|
578
|
+
const carrier = (_d = (_c = (_b = this._scene) === null || _b === void 0 ? void 0 : _b.tracker) === null || _c === void 0 ? void 0 : _c.trackingId) !== null && _d !== void 0 ? _d : '?';
|
|
579
|
+
const loc = this._carrierTrackingStore.lookupAt(timeMs);
|
|
580
|
+
this._lastFallbackResolvedAt = Date.now();
|
|
581
|
+
const logIfChanged = (target, msg) => {
|
|
582
|
+
if (target === this._lastFallbackTarget)
|
|
583
|
+
return;
|
|
584
|
+
// 이전 target 의 bouncing arrow 해제 + 새 target 에 부착 (가시성 강화).
|
|
585
|
+
// deco-border 는 things-scene Tracker 가 처리하므로 우리는 deco-arrow 만 관리.
|
|
586
|
+
this._setFallbackArrow(this._lastFallbackTarget, null);
|
|
587
|
+
this._setFallbackArrow(null, target);
|
|
588
|
+
this._lastFallbackTarget = target;
|
|
589
|
+
console.log(msg);
|
|
590
|
+
};
|
|
591
|
+
if (!loc) {
|
|
592
|
+
logIfChanged(null, `[CarrierTracking] fallback: history miss (carrier=${carrier}, t=${currentTime})`);
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
if (loc.machineName === 'NA') {
|
|
596
|
+
logIfChanged(null, `[CarrierTracking] fallback: NA (carrier=${carrier}, t=${currentTime})`);
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
// unitName 우선 (더 정밀), 없으면 machineName 으로 폴백.
|
|
600
|
+
// 같은 id 컴포넌트가 여러 개일 수 있어 (예: 머신 본체 + 부착된 게이지) findAllById
|
|
601
|
+
// 로 후보 모두 가져온 뒤, mcs-mouse-event-handler 의 클릭 dispatch 와 동일한
|
|
602
|
+
// 패턴으로 머신 본체로 수렴:
|
|
603
|
+
// - 'isLVComponent' 후보 선택
|
|
604
|
+
// - parentObject 가 있으면 그쪽으로 escalate (게이지·바 → 머신 본체)
|
|
605
|
+
// - 없으면 자기 자신 사용
|
|
606
|
+
const resolveLVById = (id) => {
|
|
607
|
+
var _a, _b;
|
|
608
|
+
const candidates = (((_a = scene === null || scene === void 0 ? void 0 : scene.findAllById) === null || _a === void 0 ? void 0 : _a.call(scene, id)) || []);
|
|
609
|
+
const c = candidates.find(c => 'isLVComponent' in c);
|
|
610
|
+
if (!c)
|
|
611
|
+
return null;
|
|
612
|
+
return (_b = c.parentObject) !== null && _b !== void 0 ? _b : c;
|
|
613
|
+
};
|
|
614
|
+
let target = null;
|
|
615
|
+
let hitVia = null;
|
|
616
|
+
if (loc.unitName) {
|
|
617
|
+
const t = resolveLVById(loc.unitName);
|
|
618
|
+
if (t) {
|
|
619
|
+
target = t;
|
|
620
|
+
hitVia = 'unit';
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
if (!target && loc.machineName) {
|
|
624
|
+
const t = resolveLVById(loc.machineName);
|
|
625
|
+
if (t) {
|
|
626
|
+
target = t;
|
|
627
|
+
hitVia = 'machine';
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (hitVia === 'unit') {
|
|
631
|
+
logIfChanged(target, `[CarrierTracking] fallback applied via unit=${loc.unitName} ` +
|
|
632
|
+
`(machine=${loc.machineName} not tried) (carrier=${carrier}, t=${currentTime})`);
|
|
633
|
+
}
|
|
634
|
+
else if (hitVia === 'machine') {
|
|
635
|
+
logIfChanged(target, `[CarrierTracking] fallback applied via machine=${loc.machineName} ` +
|
|
636
|
+
`(unit=${loc.unitName} miss) (carrier=${carrier}, t=${currentTime})`);
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
logIfChanged(target, `[CarrierTracking] fallback: scene 미존재 — unit=${loc.unitName} (miss), ` +
|
|
640
|
+
`machine=${loc.machineName} (miss) (carrier=${carrier}, t=${currentTime})`);
|
|
641
|
+
}
|
|
642
|
+
return target;
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* deco-arrow 라이프사이클 — 이전 target 끄고 새 target 에 bouncing arrow 부착.
|
|
646
|
+
* 둘 다 null 가능 (해제만, 부착만).
|
|
647
|
+
*/
|
|
648
|
+
_setFallbackArrow(off, on) {
|
|
649
|
+
var _a, _b, _c, _d;
|
|
650
|
+
if (off) {
|
|
651
|
+
try {
|
|
652
|
+
(_b = (_a = off).trigger) === null || _b === void 0 ? void 0 : _b.call(_a, 'deco-arrow-off');
|
|
653
|
+
}
|
|
654
|
+
catch (_e) { }
|
|
655
|
+
}
|
|
656
|
+
if (on) {
|
|
657
|
+
try {
|
|
658
|
+
;
|
|
659
|
+
(_d = (_c = on).trigger) === null || _d === void 0 ? void 0 : _d.call(_c, 'deco-arrow', {
|
|
660
|
+
animation: true,
|
|
661
|
+
repeat: true,
|
|
662
|
+
size: 0.75,
|
|
663
|
+
// TRACKING_COLOR(#00aaff7f) 의 색조는 유지하되 알파(7f) 만 제거해 불투명.
|
|
664
|
+
color: BoardViewer_1.TRACKING_COLOR.slice(0, 7),
|
|
665
|
+
duration: 5000
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
catch (_f) { }
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* 1초 polling — 화살표 부착 대상을 결정.
|
|
673
|
+
* 1) 플레이백 종료 / 추적 ID 없음 → 화살표 해제
|
|
674
|
+
* 2) carrier 가 scene 에 직접 존재 → **carrier 자체에 화살표 부착**
|
|
675
|
+
* (things-scene Tracker 가 carrier 발견 시 fallback 콜백 호출을 멈춰
|
|
676
|
+
* resolver 경로로는 우리가 모름. monitor 에서 직접 책임)
|
|
677
|
+
* 3) carrier 없음 + resolver 가 3초 이상 호출 안 됨 → 화살표 해제
|
|
678
|
+
* (그 외엔 resolver 의 logIfChanged 가 fallback target 으로 부착 중)
|
|
679
|
+
*/
|
|
680
|
+
_startFallbackArrowMonitor() {
|
|
681
|
+
if (this._fallbackArrowMonitor != null)
|
|
682
|
+
return;
|
|
683
|
+
this._fallbackArrowMonitor = window.setInterval(() => {
|
|
684
|
+
var _a, _b, _c, _d, _e;
|
|
685
|
+
// 1) 플레이백 종료 / 추적 ID 해제
|
|
686
|
+
const trackingId = (_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.tracker) === null || _b === void 0 ? void 0 : _b.trackingId;
|
|
687
|
+
if (!this._playbackActive || !trackingId) {
|
|
688
|
+
this._clearFallbackArrow();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
// 2) carrier 가 scene 에 직접 존재 — carrier(또는 parentObject)에 화살표 부착
|
|
692
|
+
const carrierComp = (((_d = (_c = this._scene) === null || _c === void 0 ? void 0 : _c.findAllById) === null || _d === void 0 ? void 0 : _d.call(_c, trackingId)) || [])
|
|
693
|
+
.find(c => 'isLVComponent' in c);
|
|
694
|
+
if (carrierComp) {
|
|
695
|
+
const arrowTarget = (_e = carrierComp.parentObject) !== null && _e !== void 0 ? _e : carrierComp;
|
|
696
|
+
if (arrowTarget !== this._lastFallbackTarget) {
|
|
697
|
+
this._setFallbackArrow(this._lastFallbackTarget, null);
|
|
698
|
+
this._setFallbackArrow(null, arrowTarget);
|
|
699
|
+
this._lastFallbackTarget = arrowTarget;
|
|
700
|
+
}
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
// 3) resolver 가 일정 시간 호출되지 않음 — Tracker 가 추적 중단했을 가능성
|
|
704
|
+
if (this._lastFallbackTarget && Date.now() - this._lastFallbackResolvedAt > 3000) {
|
|
705
|
+
this._clearFallbackArrow();
|
|
706
|
+
}
|
|
707
|
+
}, 1000);
|
|
708
|
+
}
|
|
709
|
+
_stopFallbackArrowMonitor() {
|
|
710
|
+
if (this._fallbackArrowMonitor != null) {
|
|
711
|
+
clearInterval(this._fallbackArrowMonitor);
|
|
712
|
+
this._fallbackArrowMonitor = undefined;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/** 부착된 deco-arrow 강제 해제 + dedupe 상태 초기화. */
|
|
716
|
+
_clearFallbackArrow() {
|
|
717
|
+
if (this._lastFallbackTarget) {
|
|
718
|
+
this._setFallbackArrow(this._lastFallbackTarget, null);
|
|
719
|
+
this._lastFallbackTarget = null;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* onSearchSubmit hook — 영구추적 carrierName 이 설정되는 순간.
|
|
724
|
+
* 플레이백 윈도우가 잡혀 있으면 즉시 store 충전. 윈도우가 없으면
|
|
725
|
+
* 플레이백 시작 시 (_onPlaybackStart) 에 충전.
|
|
726
|
+
*/
|
|
727
|
+
_onCarrierTrackingSubmit(carrierName) {
|
|
728
|
+
this._clearFallbackArrow(); // 새 carrier — 기존 화살표/dedupe 정리
|
|
729
|
+
if (!carrierName) {
|
|
730
|
+
this._carrierTrackingStore.clear();
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const win = this._playbackWindow;
|
|
734
|
+
if (this._playbackActive && win) {
|
|
735
|
+
this._carrierTrackingStore.load(carrierName, win.from, win.to);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
// 플레이백 비활성 — 윈도우 정해질 때까지 대기. 캐시는 비워둠.
|
|
739
|
+
this._carrierTrackingStore.clear();
|
|
740
|
+
}
|
|
639
741
|
}
|
|
640
742
|
unbindSceneEvents(scene) {
|
|
641
743
|
scene.off('run', this.onRunBoard, this);
|
|
@@ -779,6 +881,7 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
779
881
|
}
|
|
780
882
|
}
|
|
781
883
|
async _onPlaybackStart(e) {
|
|
884
|
+
var _a, _b;
|
|
782
885
|
const { fromTime, toTime, speed } = e.detail;
|
|
783
886
|
if (!this._playbackEngine)
|
|
784
887
|
return;
|
|
@@ -790,6 +893,15 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
790
893
|
const endTime = toTime instanceof Date ? toTime : new Date(fromTime.getTime() + 3600000);
|
|
791
894
|
if (this._scene)
|
|
792
895
|
this._scene.playbackMode = true;
|
|
896
|
+
// 플레이백 윈도우 기록 + 영구추적 활성 시 carrier history 충전.
|
|
897
|
+
// 윈도우 변경 (재시작) 시에도 같은 경로 — store.load 가 key 비교로
|
|
898
|
+
// 동일 (carrierName, from, to) 이면 no-op.
|
|
899
|
+
this._playbackWindow = { from: fromTime, to: endTime };
|
|
900
|
+
const carrierName = (_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.tracker) === null || _b === void 0 ? void 0 : _b.trackingId;
|
|
901
|
+
if (carrierName) {
|
|
902
|
+
this._carrierTrackingStore.load(carrierName, fromTime, endTime);
|
|
903
|
+
}
|
|
904
|
+
this._startFallbackArrowMonitor();
|
|
793
905
|
try {
|
|
794
906
|
await this._playbackEngine.startChunked(fromTime, endTime, speed);
|
|
795
907
|
}
|
|
@@ -812,6 +924,10 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
812
924
|
(_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.detachScene();
|
|
813
925
|
(_b = this._playbackEngine) === null || _b === void 0 ? void 0 : _b.stop();
|
|
814
926
|
this._playbackActive = false;
|
|
927
|
+
this._playbackWindow = undefined;
|
|
928
|
+
this._carrierTrackingStore.clear();
|
|
929
|
+
this._clearFallbackArrow();
|
|
930
|
+
this._stopFallbackArrowMonitor();
|
|
815
931
|
if (scene) {
|
|
816
932
|
scene.suppressAnimations = true;
|
|
817
933
|
scene.playbackMode = false;
|
|
@@ -1131,19 +1247,46 @@ BoardViewer.styles = [
|
|
|
1131
1247
|
}
|
|
1132
1248
|
|
|
1133
1249
|
[fab-group] md-fab.sub-fab {
|
|
1134
|
-
--md-fab-container-width:
|
|
1135
|
-
--md-fab-container-height:
|
|
1136
|
-
|
|
1250
|
+
--md-fab-container-width: 48px;
|
|
1251
|
+
--md-fab-container-height: 48px;
|
|
1252
|
+
/* fab 표면 색 = image 의 자체 원형 배경 (#F9EBEA) 과 일치.
|
|
1253
|
+
* 이렇게 하면 md-fab 의 내부 padding 영역도 같은 색이라
|
|
1254
|
+
* 시각적으로 사라짐 — 세 버튼 모두 visible 48px 컨테이너 통일. */
|
|
1255
|
+
--md-fab-container-color: #F9EBEA;
|
|
1256
|
+
--md-fab-icon-color: #F87171;
|
|
1257
|
+
--md-icon-size: 36px;
|
|
1137
1258
|
transform: scale(0);
|
|
1138
1259
|
opacity: 0;
|
|
1139
1260
|
pointer-events: none;
|
|
1140
1261
|
}
|
|
1141
1262
|
|
|
1142
|
-
/*
|
|
1263
|
+
/* visible 영역은 컨테이너(48px) 그대로 — fullscreen 버튼과 동일.
|
|
1264
|
+
* img 자체는 48px 박스이고 transform scale 로 내부 그래픽만 확대.
|
|
1265
|
+
* wrap 의 overflow:hidden + border-radius:50% 가 fab 원형 밖으로
|
|
1266
|
+
* 넘치는 영역을 잘라내어 외형 크기는 변하지 않음. */
|
|
1267
|
+
[fab-group] md-fab.sub-fab .fab-icon-wrap {
|
|
1268
|
+
width: 48px;
|
|
1269
|
+
height: 48px;
|
|
1270
|
+
display: block;
|
|
1271
|
+
overflow: hidden;
|
|
1272
|
+
border-radius: 50%;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
[fab-group] md-fab.sub-fab .fab-icon-wrap img.fab-icon {
|
|
1276
|
+
width: 48px;
|
|
1277
|
+
height: 48px;
|
|
1278
|
+
display: block;
|
|
1279
|
+
transform: scale(1.15);
|
|
1280
|
+
transform-origin: center;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/* legacy inline svg 경로 — 동일하게 box 48 유지 + scale 로 그래픽만 확대. */
|
|
1143
1284
|
[fab-group] md-fab.sub-fab svg[slot="icon"] {
|
|
1144
|
-
width:
|
|
1145
|
-
height:
|
|
1285
|
+
width: 48px;
|
|
1286
|
+
height: 48px;
|
|
1146
1287
|
display: block;
|
|
1288
|
+
transform: scale(1.15);
|
|
1289
|
+
transform-origin: center;
|
|
1147
1290
|
}
|
|
1148
1291
|
|
|
1149
1292
|
[fab-group][expanded] md-fab.sub-fab {
|