@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.
@@ -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
- window.dispatchEvent(new CustomEvent('fetchLvModelDataByMouseEvent', {
6
- detail: {
7
- value: {
8
- type,
9
- event,
10
- model: {
11
- ...target.state,
12
- parentObject: parent
13
- ? {
14
- id: parent.get('id'),
15
- type: parent.get('type')
16
- }
17
- : null
18
- },
19
- data: target.data
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
- window.dispatchEvent(new CustomEvent('fetchLvModelDataByMouseEvent', {
27
- detail: {
28
- value: {
29
- type,
30
- event,
31
- model: parent.state,
32
- data: parent.data
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;IAEhD,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAA;IAElC,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,8BAA8B,EAAE;YAC9C,MAAM,EAAE;gBACN,KAAK,EAAE;oBACL,IAAI;oBACJ,KAAK;oBACL,KAAK,EAAE;wBACL,GAAG,MAAM,CAAC,KAAK;wBACf,YAAY,EAAE,MAAM;4BAClB,CAAC,CAAC;gCACE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;gCACpB,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;6BACzB;4BACH,CAAC,CAAC,IAAI;qBACT;oBACD,IAAI,EAAE,MAAM,CAAC,IAAI;iBAClB;aACF;SACF,CAAC,CACH,CAAA;QAED,OAAO,MAAM,IAAI,MAAM,CAAA;IACzB,CAAC;SAAM,IAAI,MAAM,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,8BAA8B,EAAE;YAC9C,MAAM,EAAE;gBACN,KAAK,EAAE;oBACL,IAAI;oBACJ,KAAK;oBACL,KAAK,EAAE,MAAM,CAAC,KAAK;oBACnB,IAAI,EAAE,MAAM,CAAC,IAAI;iBAClB;aACF;SACF,CAAC,CACH,CAAA;QAED,OAAO,MAAM,CAAA;IACf,CAAC;AACH,CAAC;AAED,wDAAwD;AACxD,+BAA+B;AAE/B,4BAA4B;AAE5B,iEAAiE;AACjE,IAAI;AAEJ,SAAS,OAAO,CAAC,KAAiB,EAAE,IAAS;IAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;IAE1B,KAAK,CAAC,eAAe,EAAE,CAAA;IAEvB,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,MAAM,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;YACvC,MAAM,UAAU,GAAG,oCAAoC,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;YAE/E,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,gBAAgB,EAAE;gBAChC,MAAM,EAAE,UAAU,IAAI,MAAM;aAC7B,CAAC,CACH,CAAA;QACH,CAAC;IACH,CAAC,EAAE,GAAG,CAAC,CAAA;AACT,CAAC;AAED,SAAS,UAAU,CAAC,KAAiB,EAAE,IAAS;IAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;IAE1B,UAAU,CAAC,GAAG,EAAE;QACd,MAAM,CAAC,YAAY,GAAG,EAAE,CAAA;IAC1B,CAAC,EAAE,GAAG,CAAC,CAAA;IAEP,KAAK,CAAC,eAAe,EAAE,CAAA;IAEvB,MAAM,CAAC,YAAY,GAAG,UAAU,CAAA;IAEhC,oCAAoC,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;AACjE,CAAC;AAED,QAAQ,CAAC,QAAQ,CAAC,mBAAmB,EAAE;IACrC,aAAa,EAAE;QACb,OAAO,EAAE;YACP,6CAA6C;YAC7C,KAAK,EAAE,OAAO;YACd,QAAQ,EAAE,UAAU;SACrB;KACF;CACF,CAAC,CAAA","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 if (target.getState('id')) {\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 return parent || target\n } else if (parent && parent.getState('id')) {\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 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"]}
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;;IAEhD,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAA;IAElC,4DAA4D;IAC5D,oDAAoD;IACpD,+DAA+D;IAC/D,gCAAgC;IAChC,MAAM,YAAY,GAAG,CAAC,CAAC,CAAA,MAAA,MAAA,MAAC,MAAc,aAAd,MAAM,uBAAN,MAAM,CAAU,IAAI,0CAAE,GAAG,0CAAE,KAAK,0CAAE,YAAY,CAAA,CAAA;IAEtE,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,8BAA8B,EAAE;gBAC9C,MAAM,EAAE;oBACN,KAAK,EAAE;wBACL,IAAI;wBACJ,KAAK;wBACL,KAAK,EAAE;4BACL,GAAG,MAAM,CAAC,KAAK;4BACf,YAAY,EAAE,MAAM;gCAClB,CAAC,CAAC;oCACE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;oCACpB,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;iCACzB;gCACH,CAAC,CAAC,IAAI;yBACT;wBACD,IAAI,EAAE,MAAM,CAAC,IAAI;qBAClB;iBACF;aACF,CAAC,CACH,CAAA;QACH,CAAC;QAED,OAAO,MAAM,IAAI,MAAM,CAAA;IACzB,CAAC;SAAM,IAAI,MAAM,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,8BAA8B,EAAE;gBAC9C,MAAM,EAAE;oBACN,KAAK,EAAE;wBACL,IAAI;wBACJ,KAAK;wBACL,KAAK,EAAE,MAAM,CAAC,KAAK;wBACnB,IAAI,EAAE,MAAM,CAAC,IAAI;qBAClB;iBACF;aACF,CAAC,CACH,CAAA;QACH,CAAC;QAED,OAAO,MAAM,CAAA;IACf,CAAC;AACH,CAAC;AAED,wDAAwD;AACxD,+BAA+B;AAE/B,4BAA4B;AAE5B,iEAAiE;AACjE,IAAI;AAEJ,SAAS,OAAO,CAAC,KAAiB,EAAE,IAAS;IAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;IAE1B,KAAK,CAAC,eAAe,EAAE,CAAA;IAEvB,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,MAAM,CAAC,YAAY,KAAK,UAAU,EAAE,CAAC;YACvC,MAAM,UAAU,GAAG,oCAAoC,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;YAE/E,MAAM,CAAC,aAAa,CAClB,IAAI,WAAW,CAAC,gBAAgB,EAAE;gBAChC,MAAM,EAAE,UAAU,IAAI,MAAM;aAC7B,CAAC,CACH,CAAA;QACH,CAAC;IACH,CAAC,EAAE,GAAG,CAAC,CAAA;AACT,CAAC;AAED,SAAS,UAAU,CAAC,KAAiB,EAAE,IAAS;IAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAA;IAE1B,UAAU,CAAC,GAAG,EAAE;QACd,MAAM,CAAC,YAAY,GAAG,EAAE,CAAA;IAC1B,CAAC,EAAE,GAAG,CAAC,CAAA;IAEP,KAAK,CAAC,eAAe,EAAE,CAAA;IAEvB,MAAM,CAAC,YAAY,GAAG,UAAU,CAAA;IAEhC,oCAAoC,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;AACjE,CAAC;AAED,QAAQ,CAAC,QAAQ,CAAC,mBAAmB,EAAE;IACrC,aAAa,EAAE;QACb,OAAO,EAAE;YACP,6CAA6C;YAC7C,KAAK,EAAE,OAAO;YACd,QAAQ,EAAE,UAAU;SACrB;KACF;CACF,CAAC,CAAA","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
- >${this._playbackActive
235
- ? html `<svg slot="icon" width="48" height="48" viewBox="0 0 100 100" style="display: block;">
236
- <circle cx="50" cy="50" r="50" fill="#F9EBEA" />
237
- <g transform="translate(-5, 0)">
238
- <path
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
- >${this._is3dMode
303
- ? html `<svg slot="icon" width="48" height="48" viewBox="0 0 100 100" style="display: block;">
304
- <circle cx="50" cy="50" r="50" fill="#F9EBEA" />
305
- <g transform="rotate(55, 50, 50)">
306
- <path
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
- this._scene.trackerOptions = { color: BoardViewer_1.TRACKING_COLOR };
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: 40px;
1135
- --md-fab-container-height: 40px;
1136
- --md-icon-size: 20px;
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
- /* SVG icon 은 fab 전체 크기로 — 자체 원이 fab 표면을 대체. */
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: 40px;
1145
- height: 40px;
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 {