@fmsim/board 2.0.0-beta.81 → 2.0.0-beta.83
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 +1856 -1565
- package/dist/src/layers/mcs-mouse-event-handler.js +8 -0
- package/dist/src/layers/mcs-mouse-event-handler.js.map +1 -1
- package/dist/src/layers/movement-layer.js +2 -2
- package/dist/src/layers/movement-layer.js.map +1 -1
- package/dist/src/ox-board-viewer.d.ts +48 -0
- package/dist/src/ox-board-viewer.js +297 -143
- 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
|
@@ -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;
|
|
@@ -45,16 +52,16 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
45
52
|
};
|
|
46
53
|
// 모니터링에서 Movement Data를 보여준다.
|
|
47
54
|
this.fmbShowMovementHandler = (e) => {
|
|
48
|
-
var _a, _b;
|
|
55
|
+
var _a, _b, _c;
|
|
49
56
|
// 플레이백 중에는 외부 모니터링이 보내는 실시간 반송 정보를 그리지 않는다 —
|
|
50
57
|
// 사용자가 플레이백 시점의 *과거 상태* 를 보고 있는데 실시간 화살표가 겹치면
|
|
51
58
|
// 혼란을 줌. (백엔드 상세 정보 요청은 ox-board-component-info.ts:225 에서
|
|
52
59
|
// 같은 playbackMode 가드로 이미 차단됨.)
|
|
53
|
-
if ((_a = this._scene) === null || _a === void 0 ? void 0 : _a.playbackMode) {
|
|
60
|
+
if ((_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.rootContainer) === null || _b === void 0 ? void 0 : _b.playbackMode) {
|
|
54
61
|
return;
|
|
55
62
|
}
|
|
56
63
|
const movement = e.detail.value;
|
|
57
|
-
const movementLayer = (
|
|
64
|
+
const movementLayer = (_c = this._scene) === null || _c === void 0 ? void 0 : _c.findById(`movement-layer`);
|
|
58
65
|
if (!movementLayer) {
|
|
59
66
|
return;
|
|
60
67
|
}
|
|
@@ -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);
|
|
@@ -727,7 +829,7 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
727
829
|
}
|
|
728
830
|
}
|
|
729
831
|
onTapPlayback() {
|
|
730
|
-
var _a;
|
|
832
|
+
var _a, _b;
|
|
731
833
|
if (this._playbackActive) {
|
|
732
834
|
this._onPlaybackStop();
|
|
733
835
|
}
|
|
@@ -740,6 +842,11 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
740
842
|
// 2) 라이브 데이터 초기화 (깨끗한 상태에서 playback 시작)
|
|
741
843
|
this._resetSceneData();
|
|
742
844
|
this._playbackActive = true;
|
|
845
|
+
// playbackMode 를 "라이브 아님" 의미로 통일 — 준비 단계부터 set 해서
|
|
846
|
+
// FAB 클릭 직후 ~ Play 누르기 전 사이의 갭에서도 가드 (mouse handler,
|
|
847
|
+
// movement layer, restful subscription) 가 작동하도록.
|
|
848
|
+
if ((_b = this._scene) === null || _b === void 0 ? void 0 : _b.rootContainer)
|
|
849
|
+
this._scene.rootContainer.playbackMode = true;
|
|
743
850
|
if (!this._playbackEngine) {
|
|
744
851
|
this._playbackEngine = new PlaybackEngine();
|
|
745
852
|
this._playbackEngine.on('frame', ({ time }) => {
|
|
@@ -779,6 +886,7 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
779
886
|
}
|
|
780
887
|
}
|
|
781
888
|
async _onPlaybackStart(e) {
|
|
889
|
+
var _a, _b, _c;
|
|
782
890
|
const { fromTime, toTime, speed } = e.detail;
|
|
783
891
|
if (!this._playbackEngine)
|
|
784
892
|
return;
|
|
@@ -788,8 +896,19 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
788
896
|
// the requested window. Falling back to `+1h` only if toTime was
|
|
789
897
|
// omitted (legacy callers).
|
|
790
898
|
const endTime = toTime instanceof Date ? toTime : new Date(fromTime.getTime() + 3600000);
|
|
791
|
-
|
|
792
|
-
|
|
899
|
+
// playbackMode 는 onTapPlayback (준비 단계) 에서 이미 set — idempotent
|
|
900
|
+
// 재설정으로 안전망 유지.
|
|
901
|
+
if ((_a = this._scene) === null || _a === void 0 ? void 0 : _a.rootContainer)
|
|
902
|
+
this._scene.rootContainer.playbackMode = true;
|
|
903
|
+
// 플레이백 윈도우 기록 + 영구추적 활성 시 carrier history 충전.
|
|
904
|
+
// 윈도우 변경 (재시작) 시에도 같은 경로 — store.load 가 key 비교로
|
|
905
|
+
// 동일 (carrierName, from, to) 이면 no-op.
|
|
906
|
+
this._playbackWindow = { from: fromTime, to: endTime };
|
|
907
|
+
const carrierName = (_c = (_b = this._scene) === null || _b === void 0 ? void 0 : _b.tracker) === null || _c === void 0 ? void 0 : _c.trackingId;
|
|
908
|
+
if (carrierName) {
|
|
909
|
+
this._carrierTrackingStore.load(carrierName, fromTime, endTime);
|
|
910
|
+
}
|
|
911
|
+
this._startFallbackArrowMonitor();
|
|
793
912
|
try {
|
|
794
913
|
await this._playbackEngine.startChunked(fromTime, endTime, speed);
|
|
795
914
|
}
|
|
@@ -812,9 +931,14 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
812
931
|
(_a = this._playbackEngine) === null || _a === void 0 ? void 0 : _a.detachScene();
|
|
813
932
|
(_b = this._playbackEngine) === null || _b === void 0 ? void 0 : _b.stop();
|
|
814
933
|
this._playbackActive = false;
|
|
934
|
+
this._playbackWindow = undefined;
|
|
935
|
+
this._carrierTrackingStore.clear();
|
|
936
|
+
this._clearFallbackArrow();
|
|
937
|
+
this._stopFallbackArrowMonitor();
|
|
815
938
|
if (scene) {
|
|
816
939
|
scene.suppressAnimations = true;
|
|
817
|
-
scene.
|
|
940
|
+
if (scene.rootContainer)
|
|
941
|
+
scene.rootContainer.playbackMode = false;
|
|
818
942
|
}
|
|
819
943
|
// UI 신호 + fullscreen 해제
|
|
820
944
|
this.dispatchEvent(new CustomEvent('playback-toggle', {
|
|
@@ -1040,8 +1164,11 @@ let BoardViewer = BoardViewer_1 = class BoardViewer extends LitElement {
|
|
|
1040
1164
|
this.popup = popup;
|
|
1041
1165
|
}
|
|
1042
1166
|
if (this.popup) {
|
|
1043
|
-
|
|
1167
|
+
// playbackMode 를 component 보다 먼저 set — BoardComponentInfo 의 updated()
|
|
1168
|
+
// 가 component 변경을 보는 시점에 playbackMode 가 반드시 최신값이어야
|
|
1169
|
+
// detailInfo() 백엔드 호출 가드가 일관 동작.
|
|
1044
1170
|
this.popup.playbackMode = this._playbackActive;
|
|
1171
|
+
this.popup.component = component;
|
|
1045
1172
|
}
|
|
1046
1173
|
}
|
|
1047
1174
|
hidePopup() {
|
|
@@ -1131,19 +1258,46 @@ BoardViewer.styles = [
|
|
|
1131
1258
|
}
|
|
1132
1259
|
|
|
1133
1260
|
[fab-group] md-fab.sub-fab {
|
|
1134
|
-
--md-fab-container-width:
|
|
1135
|
-
--md-fab-container-height:
|
|
1136
|
-
|
|
1261
|
+
--md-fab-container-width: 48px;
|
|
1262
|
+
--md-fab-container-height: 48px;
|
|
1263
|
+
/* fab 표면 색 = image 의 자체 원형 배경 (#F9EBEA) 과 일치.
|
|
1264
|
+
* 이렇게 하면 md-fab 의 내부 padding 영역도 같은 색이라
|
|
1265
|
+
* 시각적으로 사라짐 — 세 버튼 모두 visible 48px 컨테이너 통일. */
|
|
1266
|
+
--md-fab-container-color: #F9EBEA;
|
|
1267
|
+
--md-fab-icon-color: #F87171;
|
|
1268
|
+
--md-icon-size: 36px;
|
|
1137
1269
|
transform: scale(0);
|
|
1138
1270
|
opacity: 0;
|
|
1139
1271
|
pointer-events: none;
|
|
1140
1272
|
}
|
|
1141
1273
|
|
|
1142
|
-
/*
|
|
1274
|
+
/* visible 영역은 컨테이너(48px) 그대로 — fullscreen 버튼과 동일.
|
|
1275
|
+
* img 자체는 48px 박스이고 transform scale 로 내부 그래픽만 확대.
|
|
1276
|
+
* wrap 의 overflow:hidden + border-radius:50% 가 fab 원형 밖으로
|
|
1277
|
+
* 넘치는 영역을 잘라내어 외형 크기는 변하지 않음. */
|
|
1278
|
+
[fab-group] md-fab.sub-fab .fab-icon-wrap {
|
|
1279
|
+
width: 48px;
|
|
1280
|
+
height: 48px;
|
|
1281
|
+
display: block;
|
|
1282
|
+
overflow: hidden;
|
|
1283
|
+
border-radius: 50%;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
[fab-group] md-fab.sub-fab .fab-icon-wrap img.fab-icon {
|
|
1287
|
+
width: 48px;
|
|
1288
|
+
height: 48px;
|
|
1289
|
+
display: block;
|
|
1290
|
+
transform: scale(1.15);
|
|
1291
|
+
transform-origin: center;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/* legacy inline svg 경로 — 동일하게 box 48 유지 + scale 로 그래픽만 확대. */
|
|
1143
1295
|
[fab-group] md-fab.sub-fab svg[slot="icon"] {
|
|
1144
|
-
width:
|
|
1145
|
-
height:
|
|
1296
|
+
width: 48px;
|
|
1297
|
+
height: 48px;
|
|
1146
1298
|
display: block;
|
|
1299
|
+
transform: scale(1.15);
|
|
1300
|
+
transform-origin: center;
|
|
1147
1301
|
}
|
|
1148
1302
|
|
|
1149
1303
|
[fab-group][expanded] md-fab.sub-fab {
|