@things-factory/board-ui 10.0.8 → 10.0.10

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.
@@ -2,8 +2,17 @@ export interface BoardPlacementItem {
2
2
  id: string;
3
3
  label: string;
4
4
  sub?: string;
5
+ /**
6
+ * 계층에서의 부모 — 있으면 트레이가 **트리로** 보여 줄 수 있다. 없으면 최상위.
7
+ *
8
+ * 호스트가 안 주면 예전처럼 평평한 목록이다(선택 사항). 부모를 못 찾은 항목은 **버리지 않고**
9
+ * 최상위로 올린다 — 목록에서 조용히 사라지면 저작하는 사람은 "그런 게 없네" 로 끝난다.
10
+ */
11
+ parentId?: string;
12
+ /** 종류 묶음 — 있으면 트레이가 **종류별로** 보여 줄 수 있다(같은 종류 여럿에 한 번에 배치). */
13
+ group?: string;
5
14
  }
6
- /** 한 항목의 가능한 표현(N:M) 능력×목적별 view. key=컴포넌트 타입(placed 판정 ). */
15
+ /** 한 항목의 가능한 표현(N:M). key = 호스트가 정하는 표현 식별자(같은 항목 안에서 유일해야 한다). */
7
16
  export interface BoardPlacementView {
8
17
  key: string;
9
18
  label: string;
@@ -17,6 +26,14 @@ export interface BoardPlacementProvider {
17
26
  viewsFor?(itemId: string): BoardPlacementView[];
18
27
  /** 선택한 view(또는 기본)로 추가할 things-scene 컴포넌트 model(반드시 `tag` 포함). null=표현 없음. */
19
28
  componentFor(item: BoardPlacementItem, viewKey?: string): any;
29
+ /**
30
+ * 배치된 컴포넌트 → 그것이 어느 표현인지(`viewsFor` 가 준 key). 트레이가 "이미 놓았는지" 를 판단한다.
31
+ *
32
+ * 구현하지 않으면 트레이는 컴포넌트 `type` 을 key 로 보고 비교한다 — `viewsFor` 의 key 가 type 과 다른
33
+ * 호스트는 **반드시 구현해야 한다.** 구현하지 않으면 이미 놓은 표현도 계속 "안 놓음" 으로 보이고,
34
+ * "모두 자동 배치" 가 같은 것을 또 놓는다.
35
+ */
36
+ viewKeyOfComponent?(component: any): string | null;
20
37
  }
21
38
  export declare function registerBoardPlacementProvider(p: BoardPlacementProvider): void;
22
39
  export declare function boardPlacementProvider(): BoardPlacementProvider | null;
@@ -1 +1 @@
1
- {"version":3,"file":"board-placement-palette.js","sourceRoot":"","sources":["../client/board-placement-palette.ts"],"names":[],"mappings":"AAgCA,IAAI,SAAS,GAAkC,IAAI,CAAA;AACnD,MAAM,UAAU,8BAA8B,CAAC,CAAyB;IACtE,SAAS,GAAG,CAAC,CAAA;AACf,CAAC;AACD,MAAM,UAAU,sBAAsB;IACpC,OAAO,SAAS,CAAA;AAClB,CAAC","sourcesContent":["/*\n * board 배치 팔레트 — 일반(표현-무관) 확장점. 호스트가 \"보드에 배치할 항목\"을 공급하고,\n * board-modeller 가 트레이로 노출 + place 시 컴포넌트를 생성한다. board 제품(@operato/board)은 무변경,\n * 이 통합층(board-ui)이 훅만 제공. 트윈은 이 훅의 호스트 구현자일 뿐(트윈 전용 로직 없음).\n *\n * 대칭: operato-twin map-modeller 의 \"미배치 노드 트레이\"(A2)를 board 표현으로 확장하는 일반 메커니즘.\n * place 는 board-edit 'add' op(기존 채널)로 컴포넌트를 추가하고, 컴포넌트 model 의 `tag` 로 라이브 결합\n * (board-provider 가 data(tag:) 구독). 이미 배치됨 = scene 에 같은 tag 컴포넌트 존재.\n */\nexport interface BoardPlacementItem {\n id: string // 곧 컴포넌트 tag (라이브 결합 키)\n label: string\n sub?: string\n}\n\n/** 한 항목의 가능한 표현(N:M) 능력×목적별 view. key=컴포넌트 타입(placed 판정 ). */\nexport interface BoardPlacementView {\n key: string\n label: string\n sub?: string\n}\n\nexport interface BoardPlacementProvider {\n label?: string // 트레이 제목(기본 \"미배치\")\n /** 이 board 의 배치 후보(전체). 비어있으면 트레이 숨김. boardId 로 컨텍스트 해소. */\n items(boardId: string): BoardPlacementItem[] | Promise<BoardPlacementItem[]>\n /** 항목의 가능한 표현들(능력×목적, N:M). 없으면(미지정) 단일 componentFor 로 동작. */\n viewsFor?(itemId: string): BoardPlacementView[]\n /** 선택한 view(또는 기본)로 추가할 things-scene 컴포넌트 model(반드시 `tag` 포함). null=표현 없음. */\n componentFor(item: BoardPlacementItem, viewKey?: string): any\n}\n\nlet _provider: BoardPlacementProvider | null = null\nexport function registerBoardPlacementProvider(p: BoardPlacementProvider): void {\n _provider = p\n}\nexport function boardPlacementProvider(): BoardPlacementProvider | null {\n return _provider\n}\n"]}
1
+ {"version":3,"file":"board-placement-palette.js","sourceRoot":"","sources":["../client/board-placement-palette.ts"],"names":[],"mappings":"AAiDA,IAAI,SAAS,GAAkC,IAAI,CAAA;AACnD,MAAM,UAAU,8BAA8B,CAAC,CAAyB;IACtE,SAAS,GAAG,CAAC,CAAA;AACf,CAAC;AACD,MAAM,UAAU,sBAAsB;IACpC,OAAO,SAAS,CAAA;AAClB,CAAC","sourcesContent":["/*\n * board 배치 팔레트 — 일반(표현-무관) 확장점. 호스트가 \"보드에 배치할 항목\"을 공급하고,\n * board-modeller 가 트레이로 노출 + place 시 컴포넌트를 생성한다. board 제품(@operato/board)은 무변경,\n * 이 통합층(board-ui)이 훅만 제공. 트윈은 이 훅의 호스트 구현자일 뿐(트윈 전용 로직 없음).\n *\n * 대칭: operato-twin map-modeller 의 \"미배치 노드 트레이\"(A2)를 board 표현으로 확장하는 일반 메커니즘.\n * place 는 board-edit 'add' op(기존 채널)로 컴포넌트를 추가하고, 컴포넌트 model 의 `tag` 로 라이브 결합\n * (board-provider 가 data(tag:) 구독). 이미 배치됨 = scene 에 같은 tag 컴포넌트 존재.\n */\nexport interface BoardPlacementItem {\n id: string // 곧 컴포넌트 tag (라이브 결합 키)\n label: string\n sub?: string\n /**\n * 계층에서의 부모 — 있으면 트레이가 **트리로** 보여 줄 수 있다. 없으면 최상위.\n *\n * 호스트가 안 주면 예전처럼 평평한 목록이다(선택 사항). 부모를 못 찾은 항목은 **버리지 않고**\n * 최상위로 올린다 — 목록에서 조용히 사라지면 저작하는 사람은 \"그런 게 없네\" 로 끝난다.\n */\n parentId?: string\n /** 종류 묶음 — 있으면 트레이가 **종류별로** 보여 줄 수 있다(같은 종류 여럿에 한 번에 배치). */\n group?: string\n}\n\n/** 한 항목의 가능한 표현(N:M). key = 호스트가 정하는 표현 식별자(같은 항목 안에서 유일해야 한다). */\nexport interface BoardPlacementView {\n key: string\n label: string\n sub?: string\n}\n\nexport interface BoardPlacementProvider {\n label?: string // 트레이 제목(기본 \"미배치\")\n /** 이 board 의 배치 후보(전체). 비어있으면 트레이 숨김. boardId 로 컨텍스트 해소. */\n items(boardId: string): BoardPlacementItem[] | Promise<BoardPlacementItem[]>\n /** 항목의 가능한 표현들(능력×목적, N:M). 없으면(미지정) 단일 componentFor 로 동작. */\n viewsFor?(itemId: string): BoardPlacementView[]\n /** 선택한 view(또는 기본)로 추가할 things-scene 컴포넌트 model(반드시 `tag` 포함). null=표현 없음. */\n componentFor(item: BoardPlacementItem, viewKey?: string): any\n /**\n * 배치된 컴포넌트 → 그것이 어느 표현인지(`viewsFor` 가 준 key). 트레이가 \"이미 놓았는지\" 를 판단한다.\n *\n * 구현하지 않으면 트레이는 컴포넌트 `type` 을 key 로 보고 비교한다 — `viewsFor` 의 key 가 type 과 다른\n * 호스트는 **반드시 구현해야 한다.** 구현하지 않으면 이미 놓은 표현도 계속 \"안 놓음\" 으로 보이고,\n * \"모두 자동 배치\" 가 같은 것을 또 놓는다.\n */\n viewKeyOfComponent?(component: any): string | null\n}\n\nlet _provider: BoardPlacementProvider | null = null\nexport function registerBoardPlacementProvider(p: BoardPlacementProvider): void {\n _provider = p\n}\nexport function boardPlacementProvider(): BoardPlacementProvider | null {\n return _provider\n}\n"]}
@@ -10,6 +10,13 @@ export declare class BoardPlacementTray extends LitElement {
10
10
  private _drag;
11
11
  private _timer?;
12
12
  private _dropCount;
13
+ /**
14
+ * 어떻게 볼 것인가 — **계층**(소속 트리) 또는 **종류**(같은 것끼리 묶음).
15
+ *
16
+ * 호스트가 소속·종류를 안 주면 둘 다 없으므로 예전처럼 평평한 목록이다. 기본은 계층 — 현장은 어디에
17
+ * 무엇이 있는지로 인식한다.
18
+ */
19
+ private _view;
13
20
  static styles: import("lit").CSSResult;
14
21
  connectedCallback(): void;
15
22
  disconnectedCallback(): void;
@@ -24,5 +31,9 @@ export declare class BoardPlacementTray extends LitElement {
24
31
  private get unplaced();
25
32
  private place;
26
33
  private placeAll;
34
+ /** 이 항목에 아직 안 놓은 표현이 있나 — 트리에서 "놓을 것이 있는지" 를 이걸로 가른다. */
35
+ private hasUnplaced;
36
+ /** 항목 한 줄 — 이름과 그 항목에 놓을 수 있는 표현들. */
37
+ private renderItem;
27
38
  render(): import("lit-html").TemplateResult<1>;
28
39
  }
@@ -1,6 +1,7 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
2
  import { LitElement, html, css } from 'lit';
3
3
  import { customElement, property, state } from 'lit/decorators.js';
4
+ import { hasGroups, hasHierarchy, toPlacementGroups, toPlacementTree } from './board-placement-view.js';
4
5
  import { boardPlacementProvider } from './board-placement-palette.js';
5
6
  import { dispatchBoardEditOp } from './pages/board-edit-dispatch.js';
6
7
  /*
@@ -20,6 +21,13 @@ let BoardPlacementTray = class BoardPlacementTray extends LitElement {
20
21
  this._collapsed = false;
21
22
  this._drag = null;
22
23
  this._dropCount = 0;
24
+ /**
25
+ * 어떻게 볼 것인가 — **계층**(소속 트리) 또는 **종류**(같은 것끼리 묶음).
26
+ *
27
+ * 호스트가 소속·종류를 안 주면 둘 다 없으므로 예전처럼 평평한 목록이다. 기본은 계층 — 현장은 어디에
28
+ * 무엇이 있는지로 인식한다.
29
+ */
30
+ this._view = 'tree';
23
31
  this._onDrag = (e) => {
24
32
  const d = this._drag;
25
33
  if (!d)
@@ -60,6 +68,13 @@ let BoardPlacementTray = class BoardPlacementTray extends LitElement {
60
68
  .view:hover { background: #0b8f9c12; border-color: #0b8f9c; }
61
69
  .view.placed { color: #1a8f4c; border-color: #b7e0c5; background: #1a8f4c0e; cursor: default; }
62
70
  .purpose { font-size: 9px; color: #8a959c; text-transform: uppercase; letter-spacing: .04em; }
71
+ .vsw { display: flex; gap: 4px; margin: 0 0 6px; }
72
+ .vsw button { flex: 1; padding: 3px 0; font-size: 10px; border: 1px solid var(--line, #d5dce0); background: transparent; color: inherit; border-radius: 5px; cursor: pointer; }
73
+ .vsw button.on { background: var(--accent, #17242c); color: #fff; border-color: transparent; }
74
+ /* 맥락으로 끼운 조상 — 놓을 것이 없으니 흐리게. 길을 보여 줄 뿐이다. */
75
+ .node.context > .nhead { opacity: 0.55; }
76
+ .grp { margin-bottom: 6px; }
77
+ .ghead { font-size: 10px; opacity: 0.7; margin: 4px 0 2px; }
63
78
  .done { color: #1a8f4c; font-size: 11.5px; font-family: ui-monospace, Menlo, monospace; }
64
79
  @media (prefers-color-scheme: dark) {
65
80
  .tray { background: #141b21; border-color: #26323a; scrollbar-color: rgba(255,255,255,.18) transparent; }
@@ -110,6 +125,7 @@ let BoardPlacementTray = class BoardPlacementTray extends LitElement {
110
125
  this.scanPlaced();
111
126
  }
112
127
  scanPlaced() {
128
+ const provider = boardPlacementProvider();
113
129
  const map = this.scene?.rootContainer?.refidIndexMap;
114
130
  const keys = new Set();
115
131
  const tags = new Set();
@@ -118,11 +134,13 @@ let BoardPlacementTray = class BoardPlacementTray extends LitElement {
118
134
  if (!t)
119
135
  return;
120
136
  tags.add(String(t));
121
- /* 표현별 정확 판정 = 배치 심은 viewKey(능력:목적:컴포넌트). 없으면(구 컴포넌트) type 폴백. */
122
- const vk = comp?.get?.('viewKey');
137
+ /* 표현별 판정 = 호스트가 배치된 컴포넌트에서 되짚어 준 key. 컴포넌트에 저장된 값을 읽지 않는다
138
+ (예전에 `viewKey` 심었다가 떼면서, 심는 쪽과 읽는 쪽이 어긋나 판정이 죽은 적이 있다).
139
+ 되짚기를 제공하지 않는 호스트는 `type` 을 key 로 쓴다고 보고 비교한다. */
140
+ const derived = provider?.viewKeyOfComponent?.(comp?.model ?? comp);
123
141
  const type = comp?.get?.('type') ?? comp?.model?.type;
124
- if (vk)
125
- keys.add(`${t}::${vk}`);
142
+ if (derived)
143
+ keys.add(`${t}::${derived}`);
126
144
  else if (type)
127
145
  keys.add(`${t}::${type}`);
128
146
  });
@@ -165,12 +183,38 @@ let BoardPlacementTray = class BoardPlacementTray extends LitElement {
165
183
  }
166
184
  this.scanPlaced();
167
185
  }
186
+ /** 이 항목에 아직 안 놓은 표현이 있나 — 트리에서 "놓을 것이 있는지" 를 이걸로 가른다. */
187
+ hasUnplaced(item) {
188
+ return this.viewsOf(item).some(v => !this.viewPlaced(item.id, v.key));
189
+ }
190
+ /** 항목 한 줄 — 이름과 그 항목에 놓을 수 있는 표현들. */
191
+ renderItem(it, depth = 0, context = false) {
192
+ const views = this.viewsOf(it);
193
+ return html `<div class="node ${context ? 'context' : ''}" style=${depth ? `margin-left:${depth * 12}px` : ''}>
194
+ <div class="nhead"><span class="nid">${it.label}</span>${it.sub ? html `<span class="nty">${it.sub}</span>` : ''}</div>
195
+ ${views.length
196
+ ? html `<div class="views">
197
+ ${views.map(v => {
198
+ const placed = this.viewPlaced(it.id, v.key);
199
+ return html `<button class="view ${placed ? 'placed' : ''}" ?disabled=${placed}
200
+ @click=${() => this.place(it, v.key)} title=${placed ? '배치됨' : '이 표현으로 배치'}>
201
+ <span>${placed ? '✓ ' : ''}${v.label}</span>${v.sub ? html `<span class="purpose">${v.sub}</span>` : ''}
202
+ </button>`;
203
+ })}
204
+ </div>`
205
+ : ''}
206
+ </div>`;
207
+ }
168
208
  render() {
169
209
  const p = boardPlacementProvider();
170
210
  if (!p || !this.items.length)
171
211
  return html ``;
172
212
  const un = this.unplaced;
173
213
  const co = this._collapsed;
214
+ /* 호스트가 소속·종류를 주지 않으면 뷰 전환이 의미 없다 — 예전처럼 평평하게 보여 준다. */
215
+ const tree = hasHierarchy(this.items);
216
+ const kinds = hasGroups(this.items);
217
+ const view = tree && kinds ? this._view : tree ? 'tree' : kinds ? 'kind' : 'flat';
174
218
  return html `<div class="tray ${co ? 'collapsed' : ''}" style="left:${this._pos.x}px;top:${this._pos.y}px">
175
219
  <div class="ttl" @pointerdown=${(e) => this.startDrag(e)}>
176
220
  <span class="grip">${p.label || '미배치'}</span>
@@ -181,20 +225,23 @@ let BoardPlacementTray = class BoardPlacementTray extends LitElement {
181
225
  ${co
182
226
  ? ''
183
227
  : un.length
184
- ? html `<button class="auto" @click=${() => this.placeAll()} title="각 항목의 첫 표현을 일괄 배치">모두 자동 배치</button>
185
- <div class="hint">항목의 표현(능력×목적)을 골라 배치. 여러 표현을 얹을 수 있습니다.</div>
186
- ${un.map(it => html `<div class="node">
187
- <div class="nhead"><span class="nid">${it.label}</span>${it.sub ? html `<span class="nty">${it.sub}</span>` : ''}</div>
188
- <div class="views">
189
- ${this.viewsOf(it).map(v => {
190
- const placed = this.viewPlaced(it.id, v.key);
191
- return html `<button class="view ${placed ? 'placed' : ''}" ?disabled=${placed}
192
- @click=${() => this.place(it, v.key)} title=${placed ? '배치됨' : '이 표현으로 배치'}>
193
- <span>${placed ? '✓ ' : ''}${v.label}</span>${v.sub ? html `<span class="purpose">${v.sub}</span>` : ''}
194
- </button>`;
195
- })}
196
- </div>
197
- </div>`)}`
228
+ ? html `
229
+ ${tree && kinds
230
+ ? html `<div class="vsw" @pointerdown=${(e) => e.stopPropagation()}>
231
+ <button class="${view === 'tree' ? 'on' : ''}" @click=${() => (this._view = 'tree')} title="공간 계층으로 — 어디에 무엇이 있나">계층</button>
232
+ <button class="${view === 'kind' ? 'on' : ''}" @click=${() => (this._view = 'kind')} title="종류별로 — 같은 것 여럿에 한 번에">종류</button>
233
+ </div>`
234
+ : ''}
235
+ <button class="auto" @click=${() => this.placeAll()} title="각 항목의 첫 표현을 일괄 배치">모두 자동 배치</button>
236
+ <div class="hint">항목의 표현을 골라 배치. 여러 표현을 얹을 있습니다.</div>
237
+ ${view === 'tree'
238
+ ? /* 놓을 것이 남은 항목만 남기되 **조상은 길로 함께** 보여 준다 — 트리가 끊기면 어디에
239
+ 속한 것인지 알 수 없다. */
240
+ toPlacementTree(this.items, it => this.hasUnplaced(it)).map(r => this.renderItem(r.item, r.depth, r.context))
241
+ : view === 'kind'
242
+ ? toPlacementGroups(un).map(g => html `<div class="grp"><div class="ghead">${g.group} <b>${g.items.length}</b></div>
243
+ ${g.items.map(it => this.renderItem(it))}</div>`)
244
+ : un.map(it => this.renderItem(it))}`
198
245
  : html `<div class="done">✓ 모두 배치됨</div>`}
199
246
  </div>`;
200
247
  }
@@ -227,6 +274,10 @@ __decorate([
227
274
  state(),
228
275
  __metadata("design:type", Object)
229
276
  ], BoardPlacementTray.prototype, "_collapsed", void 0);
277
+ __decorate([
278
+ state(),
279
+ __metadata("design:type", String)
280
+ ], BoardPlacementTray.prototype, "_view", void 0);
230
281
  BoardPlacementTray = __decorate([
231
282
  customElement('board-placement-tray')
232
283
  ], BoardPlacementTray);
@@ -1 +1 @@
1
- {"version":3,"file":"board-placement-tray.js","sourceRoot":"","sources":["../client/board-placement-tray.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { boardPlacementProvider, type BoardPlacementItem, type BoardPlacementView } from './board-placement-palette.js'\nimport { dispatchBoardEditOp } from './pages/board-edit-dispatch.js'\n\n/*\n * board 배치 트레이 = 능력 컴포저 UI — 등록된 BoardPlacementProvider 의 항목을 좌측 트레이로 노출.\n * 항목(엔티티)마다 가능한 **표현들(능력×목적, N:M)** 을 펼쳐 저작이 선택 배치. 1:1 드롭 아님 —\n * 한 엔티티에 여러 표현(모니터/지표/제어/에뮬레이션)을 얹을 수 있다. 표현별 배치 여부 표시(tag::type 스캔).\n * 배치 = board-edit 'add' op(model.tag=엔티티 id) → 라이브 결합(board-provider data(tag:)).\n * board 제품 무변경 — 통합층(board-ui)이 modeller 위에 얹는 일반 오버레이.\n */\n@customElement('board-placement-tray')\nexport class BoardPlacementTray extends LitElement {\n @property({ type: String }) boardId?: string\n @property({ type: Object }) scene: any\n\n @state() private items: BoardPlacementItem[] = []\n @state() private placedKeys: Set<string> = new Set() // `${tag}::${componentType}`\n @state() private placedTags: Set<string> = new Set() // tag (fallback 단일 view 판정)\n @state() private _pos = { x: 12, y: 12 }\n @state() private _collapsed = false\n private _drag: { sx: number; sy: number; ox: number; oy: number } | null = null\n private _timer?: any\n private _dropCount = 0\n\n static styles = css`\n :host { display: contents; }\n .tray { position: absolute; top: 12px; left: 12px; width: 210px; max-height: calc(100% - 24px); overflow: auto; z-index: 5; background: #fff; border: 1px solid #d5dce0; border-radius: 10px; box-shadow: 0 6px 24px rgba(0,0,0,.18); padding: 10px 11px; display: flex; flex-direction: column; gap: 7px; }\n .tray.collapsed { overflow: visible; max-height: none; }\n /* 표준 스크롤바(앱 consoleStyles 규약: 얇은 5px·투명 트랙·은은한 thumb). */\n .tray { scrollbar-width: thin; scrollbar-color: rgba(0,0,0,.2) transparent; }\n .tray::-webkit-scrollbar { width: 5px; height: 5px; }\n .tray::-webkit-scrollbar-track { background: transparent; }\n .tray::-webkit-scrollbar-thumb { background: rgba(0,0,0,.2); border-radius: 3px; }\n .tray::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,.38); }\n .ttl { font-family: ui-monospace, Menlo, monospace; font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: #57676f; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; touch-action: none; }\n .ttl b { color: #0b8f9c; font-weight: 700; }\n .ttl .grip::before { content: '⠿'; color: #8a959c; font-size: 12px; margin-right: 5px; }\n .rt { display: inline-flex; align-items: center; gap: 6px; }\n .collapse { border: 0; background: transparent; color: #57676f; cursor: pointer; font-size: 12px; line-height: 1; padding: 2px 3px; }\n .collapse:hover { color: #0b8f9c; }\n .hint { font-size: 10.5px; color: #8a959c; font-family: ui-monospace, Menlo, monospace; margin: 0; }\n .auto { border: 1px solid #0b8f9c88; background: #0b8f9c14; color: #0b8f9c; border-radius: 7px; padding: 6px 9px; font-size: 11.5px; font-weight: 640; cursor: pointer; font-family: ui-monospace, Menlo, monospace; }\n .auto:hover { background: #0b8f9c26; }\n .node { border: 1px solid #d5dce0; border-radius: 8px; padding: 6px 8px; display: flex; flex-direction: column; gap: 5px; font-family: ui-monospace, Menlo, monospace; }\n .nhead { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; }\n .nid { font-size: 12px; font-weight: 640; color: #17242c; }\n .nty { font-size: 9.5px; color: #8a959c; }\n .views { display: flex; flex-direction: column; gap: 3px; }\n .view { display: flex; justify-content: space-between; align-items: center; gap: 6px; text-align: left; background: transparent; border: 1px solid #cfe6e9; color: #0b8f9c; border-radius: 6px; padding: 4px 7px; font-size: 11px; cursor: pointer; font-family: ui-monospace, Menlo, monospace; }\n .view:hover { background: #0b8f9c12; border-color: #0b8f9c; }\n .view.placed { color: #1a8f4c; border-color: #b7e0c5; background: #1a8f4c0e; cursor: default; }\n .purpose { font-size: 9px; color: #8a959c; text-transform: uppercase; letter-spacing: .04em; }\n .done { color: #1a8f4c; font-size: 11.5px; font-family: ui-monospace, Menlo, monospace; }\n @media (prefers-color-scheme: dark) {\n .tray { background: #141b21; border-color: #26323a; scrollbar-color: rgba(255,255,255,.18) transparent; }\n .tray::-webkit-scrollbar-thumb { background: rgba(255,255,255,.18); }\n .tray::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.34); }\n .ttl { color: #8b9aa3; } .ttl b { color: #22c3cf; }\n .node { border-color: #26323a; } .nid { color: #dde7ec; } .nty { color: #8b9aa3; }\n .view { border-color: #234; color: #22c3cf; } .view:hover { background: #22c3cf16; }\n .auto { border-color: #22c3cf88; background: #22c3cf1e; color: #22c3cf; }\n }\n `\n\n connectedCallback() {\n super.connectedCallback()\n this.reload()\n this._timer = setInterval(() => this.scanPlaced(), 2000)\n }\n disconnectedCallback() {\n super.disconnectedCallback()\n clearInterval(this._timer)\n window.removeEventListener('pointermove', this._onDrag)\n window.removeEventListener('pointerup', this._endDrag)\n }\n updated(changed: Map<string, unknown>) {\n if (changed.has('boardId')) this.reload()\n else if (changed.has('scene')) this.scanPlaced()\n }\n\n /* 헤더 드래그 이동(편집 공간 가림 방지) — 델타 기반. */\n private startDrag(e: PointerEvent) {\n e.preventDefault()\n this._drag = { sx: e.clientX, sy: e.clientY, ox: this._pos.x, oy: this._pos.y }\n window.addEventListener('pointermove', this._onDrag)\n window.addEventListener('pointerup', this._endDrag)\n }\n private _onDrag = (e: PointerEvent) => {\n const d = this._drag\n if (!d) return\n this._pos = { x: Math.max(0, d.ox + (e.clientX - d.sx)), y: Math.max(0, d.oy + (e.clientY - d.sy)) }\n }\n private _endDrag = () => {\n this._drag = null\n window.removeEventListener('pointermove', this._onDrag)\n window.removeEventListener('pointerup', this._endDrag)\n }\n\n private async reload() {\n const p = boardPlacementProvider()\n if (!p || !this.boardId) { this.items = []; return }\n try { this.items = await p.items(this.boardId) } catch { this.items = [] }\n this.scanPlaced()\n }\n private scanPlaced() {\n const map = this.scene?.rootContainer?.refidIndexMap\n const keys = new Set<string>()\n const tags = new Set<string>()\n map?.forEach?.((comp: any) => {\n const t = comp?.get?.('tag')\n if (!t) return\n tags.add(String(t))\n /* 표현별 정확 판정 = 배치 시 심은 viewKey(능력:목적:컴포넌트). 없으면(구 컴포넌트) type 로 폴백. */\n const vk = comp?.get?.('viewKey')\n const type = comp?.get?.('type') ?? comp?.model?.type\n if (vk) keys.add(`${t}::${vk}`)\n else if (type) keys.add(`${t}::${type}`)\n })\n this.placedKeys = keys\n this.placedTags = tags\n this.requestUpdate()\n }\n\n /* 항목의 표현들(능력×목적). provider 가 viewsFor 지원하면 그 결과(빈 배열=표현 없음)를 그대로,\n 미지원이면 단일 fallback. → 표현 없는 엔티티는 죽은 버튼 없이 자연히 트레이에서 빠짐. */\n private viewsOf(item: BoardPlacementItem): BoardPlacementView[] {\n const p = boardPlacementProvider()\n if (p?.viewsFor) return p.viewsFor(item.id)\n return [{ key: '', label: '배치' }]\n }\n private viewPlaced(id: string, key: string): boolean {\n return key ? this.placedKeys.has(`${id}::${key}`) : this.placedTags.has(id)\n }\n /* 미배치(=아직 안 놓은 표현이 하나라도 있는) 항목. */\n private get unplaced(): BoardPlacementItem[] {\n return this.items.filter(i => this.viewsOf(i).some(v => !this.viewPlaced(i.id, v.key)))\n }\n\n private place(item: BoardPlacementItem, viewKey: string) {\n const p = boardPlacementProvider()\n if (!p || !this.scene) return\n const comp = p.componentFor(item, viewKey || undefined)\n if (!comp) return\n const off = (this._dropCount++ % 6) * 22\n dispatchBoardEditOp(this.scene, { op: 'add', component: { ...comp, left: 80 + off, top: 80 + off } } as any)\n this.scanPlaced()\n }\n /* 모두 자동 배치 — 각 항목의 첫 미배치 표현을 배치(빠른 시작). */\n private placeAll() {\n for (const it of this.unplaced) {\n const v = this.viewsOf(it).find(v => !this.viewPlaced(it.id, v.key))\n if (v) this.place(it, v.key)\n }\n this.scanPlaced()\n }\n\n render() {\n const p = boardPlacementProvider()\n if (!p || !this.items.length) return html``\n const un = this.unplaced\n const co = this._collapsed\n return html`<div class=\"tray ${co ? 'collapsed' : ''}\" style=\"left:${this._pos.x}px;top:${this._pos.y}px\">\n <div class=\"ttl\" @pointerdown=${(e: PointerEvent) => this.startDrag(e)}>\n <span class=\"grip\">${p.label || '미배치'}</span>\n <span class=\"rt\"><b>${un.length}</b>\n <button class=\"collapse\" @pointerdown=${(e: Event) => e.stopPropagation()} @click=${() => (this._collapsed = !co)} title=${co ? '펼치기' : '접기'}>${co ? '▸' : '▾'}</button>\n </span>\n </div>\n ${co\n ? ''\n : un.length\n ? html`<button class=\"auto\" @click=${() => this.placeAll()} title=\"각 항목의 첫 표현을 일괄 배치\">모두 자동 배치</button>\n <div class=\"hint\">항목의 표현(능력×목적)을 골라 배치. 여러 표현을 얹을 수 있습니다.</div>\n ${un.map(it => html`<div class=\"node\">\n <div class=\"nhead\"><span class=\"nid\">${it.label}</span>${it.sub ? html`<span class=\"nty\">${it.sub}</span>` : ''}</div>\n <div class=\"views\">\n ${this.viewsOf(it).map(v => {\n const placed = this.viewPlaced(it.id, v.key)\n return html`<button class=\"view ${placed ? 'placed' : ''}\" ?disabled=${placed}\n @click=${() => this.place(it, v.key)} title=${placed ? '배치됨' : '이 표현으로 배치'}>\n <span>${placed ? '✓ ' : ''}${v.label}</span>${v.sub ? html`<span class=\"purpose\">${v.sub}</span>` : ''}\n </button>`\n })}\n </div>\n </div>`)}`\n : html`<div class=\"done\">✓ 모두 배치됨</div>`}\n </div>`\n }\n}\n"]}
1
+ {"version":3,"file":"board-placement-tray.js","sourceRoot":"","sources":["../client/board-placement-tray.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, html, css } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { hasGroups, hasHierarchy, toPlacementGroups, toPlacementTree } from './board-placement-view.js'\nimport { boardPlacementProvider, type BoardPlacementItem, type BoardPlacementView } from './board-placement-palette.js'\nimport { dispatchBoardEditOp } from './pages/board-edit-dispatch.js'\n\n/*\n * board 배치 트레이 = 능력 컴포저 UI — 등록된 BoardPlacementProvider 의 항목을 좌측 트레이로 노출.\n * 항목(엔티티)마다 가능한 **표현들(능력×목적, N:M)** 을 펼쳐 저작이 선택 배치. 1:1 드롭 아님 —\n * 한 엔티티에 여러 표현(모니터/지표/제어/에뮬레이션)을 얹을 수 있다. 표현별 배치 여부 표시(tag::type 스캔).\n * 배치 = board-edit 'add' op(model.tag=엔티티 id) → 라이브 결합(board-provider data(tag:)).\n * board 제품 무변경 — 통합층(board-ui)이 modeller 위에 얹는 일반 오버레이.\n */\n@customElement('board-placement-tray')\nexport class BoardPlacementTray extends LitElement {\n @property({ type: String }) boardId?: string\n @property({ type: Object }) scene: any\n\n @state() private items: BoardPlacementItem[] = []\n @state() private placedKeys: Set<string> = new Set() // `${tag}::${componentType}`\n @state() private placedTags: Set<string> = new Set() // tag (fallback 단일 view 판정)\n @state() private _pos = { x: 12, y: 12 }\n @state() private _collapsed = false\n private _drag: { sx: number; sy: number; ox: number; oy: number } | null = null\n private _timer?: any\n private _dropCount = 0\n /**\n * 어떻게 볼 것인가 — **계층**(소속 트리) 또는 **종류**(같은 것끼리 묶음).\n *\n * 호스트가 소속·종류를 안 주면 둘 다 없으므로 예전처럼 평평한 목록이다. 기본은 계층 — 현장은 어디에\n * 무엇이 있는지로 인식한다.\n */\n @state() private _view: 'tree' | 'kind' = 'tree'\n\n static styles = css`\n :host { display: contents; }\n .tray { position: absolute; top: 12px; left: 12px; width: 210px; max-height: calc(100% - 24px); overflow: auto; z-index: 5; background: #fff; border: 1px solid #d5dce0; border-radius: 10px; box-shadow: 0 6px 24px rgba(0,0,0,.18); padding: 10px 11px; display: flex; flex-direction: column; gap: 7px; }\n .tray.collapsed { overflow: visible; max-height: none; }\n /* 표준 스크롤바(앱 consoleStyles 규약: 얇은 5px·투명 트랙·은은한 thumb). */\n .tray { scrollbar-width: thin; scrollbar-color: rgba(0,0,0,.2) transparent; }\n .tray::-webkit-scrollbar { width: 5px; height: 5px; }\n .tray::-webkit-scrollbar-track { background: transparent; }\n .tray::-webkit-scrollbar-thumb { background: rgba(0,0,0,.2); border-radius: 3px; }\n .tray::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,.38); }\n .ttl { font-family: ui-monospace, Menlo, monospace; font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: #57676f; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; touch-action: none; }\n .ttl b { color: #0b8f9c; font-weight: 700; }\n .ttl .grip::before { content: '⠿'; color: #8a959c; font-size: 12px; margin-right: 5px; }\n .rt { display: inline-flex; align-items: center; gap: 6px; }\n .collapse { border: 0; background: transparent; color: #57676f; cursor: pointer; font-size: 12px; line-height: 1; padding: 2px 3px; }\n .collapse:hover { color: #0b8f9c; }\n .hint { font-size: 10.5px; color: #8a959c; font-family: ui-monospace, Menlo, monospace; margin: 0; }\n .auto { border: 1px solid #0b8f9c88; background: #0b8f9c14; color: #0b8f9c; border-radius: 7px; padding: 6px 9px; font-size: 11.5px; font-weight: 640; cursor: pointer; font-family: ui-monospace, Menlo, monospace; }\n .auto:hover { background: #0b8f9c26; }\n .node { border: 1px solid #d5dce0; border-radius: 8px; padding: 6px 8px; display: flex; flex-direction: column; gap: 5px; font-family: ui-monospace, Menlo, monospace; }\n .nhead { display: flex; justify-content: space-between; align-items: baseline; gap: 6px; }\n .nid { font-size: 12px; font-weight: 640; color: #17242c; }\n .nty { font-size: 9.5px; color: #8a959c; }\n .views { display: flex; flex-direction: column; gap: 3px; }\n .view { display: flex; justify-content: space-between; align-items: center; gap: 6px; text-align: left; background: transparent; border: 1px solid #cfe6e9; color: #0b8f9c; border-radius: 6px; padding: 4px 7px; font-size: 11px; cursor: pointer; font-family: ui-monospace, Menlo, monospace; }\n .view:hover { background: #0b8f9c12; border-color: #0b8f9c; }\n .view.placed { color: #1a8f4c; border-color: #b7e0c5; background: #1a8f4c0e; cursor: default; }\n .purpose { font-size: 9px; color: #8a959c; text-transform: uppercase; letter-spacing: .04em; }\n .vsw { display: flex; gap: 4px; margin: 0 0 6px; }\n .vsw button { flex: 1; padding: 3px 0; font-size: 10px; border: 1px solid var(--line, #d5dce0); background: transparent; color: inherit; border-radius: 5px; cursor: pointer; }\n .vsw button.on { background: var(--accent, #17242c); color: #fff; border-color: transparent; }\n /* 맥락으로 끼운 조상 — 놓을 것이 없으니 흐리게. 길을 보여 줄 뿐이다. */\n .node.context > .nhead { opacity: 0.55; }\n .grp { margin-bottom: 6px; }\n .ghead { font-size: 10px; opacity: 0.7; margin: 4px 0 2px; }\n .done { color: #1a8f4c; font-size: 11.5px; font-family: ui-monospace, Menlo, monospace; }\n @media (prefers-color-scheme: dark) {\n .tray { background: #141b21; border-color: #26323a; scrollbar-color: rgba(255,255,255,.18) transparent; }\n .tray::-webkit-scrollbar-thumb { background: rgba(255,255,255,.18); }\n .tray::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,.34); }\n .ttl { color: #8b9aa3; } .ttl b { color: #22c3cf; }\n .node { border-color: #26323a; } .nid { color: #dde7ec; } .nty { color: #8b9aa3; }\n .view { border-color: #234; color: #22c3cf; } .view:hover { background: #22c3cf16; }\n .auto { border-color: #22c3cf88; background: #22c3cf1e; color: #22c3cf; }\n }\n `\n\n connectedCallback() {\n super.connectedCallback()\n this.reload()\n this._timer = setInterval(() => this.scanPlaced(), 2000)\n }\n disconnectedCallback() {\n super.disconnectedCallback()\n clearInterval(this._timer)\n window.removeEventListener('pointermove', this._onDrag)\n window.removeEventListener('pointerup', this._endDrag)\n }\n updated(changed: Map<string, unknown>) {\n if (changed.has('boardId')) this.reload()\n else if (changed.has('scene')) this.scanPlaced()\n }\n\n /* 헤더 드래그 이동(편집 공간 가림 방지) — 델타 기반. */\n private startDrag(e: PointerEvent) {\n e.preventDefault()\n this._drag = { sx: e.clientX, sy: e.clientY, ox: this._pos.x, oy: this._pos.y }\n window.addEventListener('pointermove', this._onDrag)\n window.addEventListener('pointerup', this._endDrag)\n }\n private _onDrag = (e: PointerEvent) => {\n const d = this._drag\n if (!d) return\n this._pos = { x: Math.max(0, d.ox + (e.clientX - d.sx)), y: Math.max(0, d.oy + (e.clientY - d.sy)) }\n }\n private _endDrag = () => {\n this._drag = null\n window.removeEventListener('pointermove', this._onDrag)\n window.removeEventListener('pointerup', this._endDrag)\n }\n\n private async reload() {\n const p = boardPlacementProvider()\n if (!p || !this.boardId) { this.items = []; return }\n try { this.items = await p.items(this.boardId) } catch { this.items = [] }\n this.scanPlaced()\n }\n private scanPlaced() {\n const provider = boardPlacementProvider()\n const map = this.scene?.rootContainer?.refidIndexMap\n const keys = new Set<string>()\n const tags = new Set<string>()\n map?.forEach?.((comp: any) => {\n const t = comp?.get?.('tag')\n if (!t) return\n tags.add(String(t))\n /* 표현별 판정 = 호스트가 배치된 컴포넌트에서 되짚어 준 key. 컴포넌트에 저장된 값을 읽지 않는다\n (예전에 `viewKey` 를 심었다가 떼면서, 심는 쪽과 읽는 쪽이 어긋나 판정이 죽은 적이 있다).\n 되짚기를 제공하지 않는 호스트는 `type` 을 key 로 쓴다고 보고 비교한다. */\n const derived = provider?.viewKeyOfComponent?.(comp?.model ?? comp)\n const type = comp?.get?.('type') ?? comp?.model?.type\n if (derived) keys.add(`${t}::${derived}`)\n else if (type) keys.add(`${t}::${type}`)\n })\n this.placedKeys = keys\n this.placedTags = tags\n this.requestUpdate()\n }\n\n /* 항목의 표현들(능력×목적). provider 가 viewsFor 지원하면 그 결과(빈 배열=표현 없음)를 그대로,\n 미지원이면 단일 fallback. → 표현 없는 엔티티는 죽은 버튼 없이 자연히 트레이에서 빠짐. */\n private viewsOf(item: BoardPlacementItem): BoardPlacementView[] {\n const p = boardPlacementProvider()\n if (p?.viewsFor) return p.viewsFor(item.id)\n return [{ key: '', label: '배치' }]\n }\n private viewPlaced(id: string, key: string): boolean {\n return key ? this.placedKeys.has(`${id}::${key}`) : this.placedTags.has(id)\n }\n /* 미배치(=아직 안 놓은 표현이 하나라도 있는) 항목. */\n private get unplaced(): BoardPlacementItem[] {\n return this.items.filter(i => this.viewsOf(i).some(v => !this.viewPlaced(i.id, v.key)))\n }\n\n private place(item: BoardPlacementItem, viewKey: string) {\n const p = boardPlacementProvider()\n if (!p || !this.scene) return\n const comp = p.componentFor(item, viewKey || undefined)\n if (!comp) return\n const off = (this._dropCount++ % 6) * 22\n dispatchBoardEditOp(this.scene, { op: 'add', component: { ...comp, left: 80 + off, top: 80 + off } } as any)\n this.scanPlaced()\n }\n /* 모두 자동 배치 — 각 항목의 첫 미배치 표현을 배치(빠른 시작). */\n private placeAll() {\n for (const it of this.unplaced) {\n const v = this.viewsOf(it).find(v => !this.viewPlaced(it.id, v.key))\n if (v) this.place(it, v.key)\n }\n this.scanPlaced()\n }\n\n /** 이 항목에 아직 안 놓은 표현이 있나 — 트리에서 \"놓을 것이 있는지\" 를 이걸로 가른다. */\n private hasUnplaced(item: BoardPlacementItem): boolean {\n return this.viewsOf(item).some(v => !this.viewPlaced(item.id, v.key))\n }\n\n /** 항목 한 줄 — 이름과 그 항목에 놓을 수 있는 표현들. */\n private renderItem(it: BoardPlacementItem, depth = 0, context = false) {\n const views = this.viewsOf(it)\n return html`<div class=\"node ${context ? 'context' : ''}\" style=${depth ? `margin-left:${depth * 12}px` : ''}>\n <div class=\"nhead\"><span class=\"nid\">${it.label}</span>${it.sub ? html`<span class=\"nty\">${it.sub}</span>` : ''}</div>\n ${views.length\n ? html`<div class=\"views\">\n ${views.map(v => {\n const placed = this.viewPlaced(it.id, v.key)\n return html`<button class=\"view ${placed ? 'placed' : ''}\" ?disabled=${placed}\n @click=${() => this.place(it, v.key)} title=${placed ? '배치됨' : '이 표현으로 배치'}>\n <span>${placed ? '✓ ' : ''}${v.label}</span>${v.sub ? html`<span class=\"purpose\">${v.sub}</span>` : ''}\n </button>`\n })}\n </div>`\n : ''}\n </div>`\n }\n\n render() {\n const p = boardPlacementProvider()\n if (!p || !this.items.length) return html``\n const un = this.unplaced\n const co = this._collapsed\n /* 호스트가 소속·종류를 주지 않으면 뷰 전환이 의미 없다 — 예전처럼 평평하게 보여 준다. */\n const tree = hasHierarchy(this.items)\n const kinds = hasGroups(this.items)\n const view = tree && kinds ? this._view : tree ? 'tree' : kinds ? 'kind' : 'flat'\n\n return html`<div class=\"tray ${co ? 'collapsed' : ''}\" style=\"left:${this._pos.x}px;top:${this._pos.y}px\">\n <div class=\"ttl\" @pointerdown=${(e: PointerEvent) => this.startDrag(e)}>\n <span class=\"grip\">${p.label || '미배치'}</span>\n <span class=\"rt\"><b>${un.length}</b>\n <button class=\"collapse\" @pointerdown=${(e: Event) => e.stopPropagation()} @click=${() => (this._collapsed = !co)} title=${co ? '펼치기' : '접기'}>${co ? '▸' : '▾'}</button>\n </span>\n </div>\n ${co\n ? ''\n : un.length\n ? html`\n ${tree && kinds\n ? html`<div class=\"vsw\" @pointerdown=${(e: Event) => e.stopPropagation()}>\n <button class=\"${view === 'tree' ? 'on' : ''}\" @click=${() => (this._view = 'tree')} title=\"공간 계층으로 — 어디에 무엇이 있나\">계층</button>\n <button class=\"${view === 'kind' ? 'on' : ''}\" @click=${() => (this._view = 'kind')} title=\"종류별로 — 같은 것 여럿에 한 번에\">종류</button>\n </div>`\n : ''}\n <button class=\"auto\" @click=${() => this.placeAll()} title=\"각 항목의 첫 표현을 일괄 배치\">모두 자동 배치</button>\n <div class=\"hint\">항목의 표현을 골라 배치. 여러 표현을 얹을 수 있습니다.</div>\n ${view === 'tree'\n ? /* 놓을 것이 남은 항목만 남기되 **조상은 길로 함께** 보여 준다 — 트리가 끊기면 어디에\n 속한 것인지 알 수 없다. */\n toPlacementTree(this.items, it => this.hasUnplaced(it)).map(r => this.renderItem(r.item, r.depth, r.context))\n : view === 'kind'\n ? toPlacementGroups(un).map(\n g => html`<div class=\"grp\"><div class=\"ghead\">${g.group} <b>${g.items.length}</b></div>\n ${g.items.map(it => this.renderItem(it))}</div>`\n )\n : un.map(it => this.renderItem(it))}`\n : html`<div class=\"done\">✓ 모두 배치됨</div>`}\n </div>`\n }\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import type { BoardPlacementItem } from './board-placement-palette.js';
2
+ export interface PlacementRow {
3
+ item: BoardPlacementItem;
4
+ /** 들여쓰기 깊이. 화면은 이 값만 곱하면 된다. */
5
+ depth: number;
6
+ /** 길을 보여 주려고 끼운 조상 — 그 자체는 놓을 것이 없다. */
7
+ context?: boolean;
8
+ }
9
+ /**
10
+ * 계층 뷰 — 부모 순서로 늘어놓고 깊이를 함께 준다.
11
+ *
12
+ * `keep` 을 주면 그 항목들만 남기되 **조상은 길로 함께 남긴다**(맥락). 조상을 빼면 트리가 끊겨 어디에
13
+ * 속한 것인지 알 수 없다. 부모를 못 찾은 항목은 버리지 않고 최상위로 올린다.
14
+ */
15
+ export declare function toPlacementTree(items: readonly BoardPlacementItem[], keep?: (item: BoardPlacementItem) => boolean): PlacementRow[];
16
+ /**
17
+ * 종류 뷰 — `group` 이 있는 항목만 묶는다.
18
+ *
19
+ * `group` 이 없는 항목(계층 단계 같은 것)은 **종류가 아니므로** 여기 나오지 않는다. 그것들은 계층 뷰에서
20
+ * 고른다. 묶음 이름순으로 낸다.
21
+ */
22
+ export declare function toPlacementGroups(items: readonly BoardPlacementItem[]): {
23
+ group: string;
24
+ items: BoardPlacementItem[];
25
+ }[];
26
+ /** 두 뷰를 쓸 수 있는 목록인가 — 호스트가 소속·종류를 주지 않으면 예전처럼 평평하게 보여 준다. */
27
+ export declare function hasHierarchy(items: readonly BoardPlacementItem[]): boolean;
28
+ export declare function hasGroups(items: readonly BoardPlacementItem[]): boolean;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * 계층 뷰 — 부모 순서로 늘어놓고 깊이를 함께 준다.
3
+ *
4
+ * `keep` 을 주면 그 항목들만 남기되 **조상은 길로 함께 남긴다**(맥락). 조상을 빼면 트리가 끊겨 어디에
5
+ * 속한 것인지 알 수 없다. 부모를 못 찾은 항목은 버리지 않고 최상위로 올린다.
6
+ */
7
+ export function toPlacementTree(items, keep) {
8
+ const byId = new Map(items.map(i => [i.id, i]));
9
+ const parentOf = (i) => (i.parentId && byId.has(i.parentId) ? i.parentId : '');
10
+ /* 남길 것 + 그 조상들. 조상은 맥락으로 표시해 "놓을 것이 있는 항목" 과 구별한다. */
11
+ let visible = null;
12
+ const context = new Set();
13
+ if (keep) {
14
+ visible = new Set(items.filter(keep).map(i => i.id));
15
+ for (const id of [...visible]) {
16
+ let p = parentOf(byId.get(id));
17
+ while (p && !visible.has(p) && !context.has(p)) {
18
+ context.add(p);
19
+ p = parentOf(byId.get(p));
20
+ }
21
+ }
22
+ }
23
+ const children = new Map();
24
+ for (const it of items) {
25
+ if (visible && !visible.has(it.id) && !context.has(it.id))
26
+ continue;
27
+ const p = parentOf(it);
28
+ if (!children.has(p))
29
+ children.set(p, []);
30
+ children.get(p).push(it);
31
+ }
32
+ const rows = [];
33
+ const walk = (parent, depth) => {
34
+ for (const it of children.get(parent) ?? []) {
35
+ rows.push({ item: it, depth, ...(context.has(it.id) ? { context: true } : {}) });
36
+ walk(it.id, depth + 1);
37
+ }
38
+ };
39
+ walk('', 0);
40
+ return rows;
41
+ }
42
+ /**
43
+ * 종류 뷰 — `group` 이 있는 항목만 묶는다.
44
+ *
45
+ * `group` 이 없는 항목(계층 단계 같은 것)은 **종류가 아니므로** 여기 나오지 않는다. 그것들은 계층 뷰에서
46
+ * 고른다. 묶음 이름순으로 낸다.
47
+ */
48
+ export function toPlacementGroups(items) {
49
+ const by = new Map();
50
+ for (const it of items) {
51
+ if (!it.group)
52
+ continue;
53
+ if (!by.has(it.group))
54
+ by.set(it.group, []);
55
+ by.get(it.group).push(it);
56
+ }
57
+ return [...by.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([group, items]) => ({ group, items }));
58
+ }
59
+ /** 두 뷰를 쓸 수 있는 목록인가 — 호스트가 소속·종류를 주지 않으면 예전처럼 평평하게 보여 준다. */
60
+ export function hasHierarchy(items) {
61
+ return items.some(i => !!i.parentId);
62
+ }
63
+ export function hasGroups(items) {
64
+ return items.some(i => !!i.group);
65
+ }
66
+ //# sourceMappingURL=board-placement-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-placement-view.js","sourceRoot":"","sources":["../client/board-placement-view.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 배치 대상 목록을 **두 가지로 보는** 방법 — 순수.\n *\n * 호스트는 항목만 공급하고(`BoardPlacementItem`), 어떻게 볼지는 보드 층이 정한다. 항목에 `parentId`\n * (소속)와 `group`(종류)이 달려 있으면 두 뷰가 성립한다.\n *\n * 계층 뷰 소속 트리 — \"이 자리에 무엇을 놓을까\". 현장 감각으로 찾는다.\n * 종류 뷰 같은 종류끼리 묶음 — \"같은 것 여럿에 한 번에\". 대량 저작의 단위다.\n *\n * 둘 다 **항목을 잃지 않는다.** 목록에서 조용히 빠지면 저작하는 사람은 \"그런 게 없네\" 로 끝난다 —\n * 오류가 아니라서 아무도 모른다.\n */\nimport type { BoardPlacementItem } from './board-placement-palette.js'\n\nexport interface PlacementRow {\n item: BoardPlacementItem\n /** 들여쓰기 깊이. 화면은 이 값만 곱하면 된다. */\n depth: number\n /** 길을 보여 주려고 끼운 조상 — 그 자체는 놓을 것이 없다. */\n context?: boolean\n}\n\n/**\n * 계층 뷰 — 부모 순서로 늘어놓고 깊이를 함께 준다.\n *\n * `keep` 을 주면 그 항목들만 남기되 **조상은 길로 함께 남긴다**(맥락). 조상을 빼면 트리가 끊겨 어디에\n * 속한 것인지 알 수 없다. 부모를 못 찾은 항목은 버리지 않고 최상위로 올린다.\n */\nexport function toPlacementTree(items: readonly BoardPlacementItem[], keep?: (item: BoardPlacementItem) => boolean): PlacementRow[] {\n const byId = new Map(items.map(i => [i.id, i]))\n const parentOf = (i: BoardPlacementItem) => (i.parentId && byId.has(i.parentId) ? i.parentId : '')\n\n /* 남길 것 + 그 조상들. 조상은 맥락으로 표시해 \"놓을 것이 있는 항목\" 과 구별한다. */\n let visible: Set<string> | null = null\n const context = new Set<string>()\n if (keep) {\n visible = new Set(items.filter(keep).map(i => i.id))\n for (const id of [...visible]) {\n let p = parentOf(byId.get(id)!)\n while (p && !visible.has(p) && !context.has(p)) {\n context.add(p)\n p = parentOf(byId.get(p)!)\n }\n }\n }\n\n const children = new Map<string, BoardPlacementItem[]>()\n for (const it of items) {\n if (visible && !visible.has(it.id) && !context.has(it.id)) continue\n const p = parentOf(it)\n if (!children.has(p)) children.set(p, [])\n children.get(p)!.push(it)\n }\n\n const rows: PlacementRow[] = []\n const walk = (parent: string, depth: number) => {\n for (const it of children.get(parent) ?? []) {\n rows.push({ item: it, depth, ...(context.has(it.id) ? { context: true } : {}) })\n walk(it.id, depth + 1)\n }\n }\n walk('', 0)\n\n return rows\n}\n\n/**\n * 종류 뷰 — `group` 이 있는 항목만 묶는다.\n *\n * `group` 이 없는 항목(계층 단계 같은 것)은 **종류가 아니므로** 여기 나오지 않는다. 그것들은 계층 뷰에서\n * 고른다. 묶음 이름순으로 낸다.\n */\nexport function toPlacementGroups(items: readonly BoardPlacementItem[]): { group: string; items: BoardPlacementItem[] }[] {\n const by = new Map<string, BoardPlacementItem[]>()\n for (const it of items) {\n if (!it.group) continue\n if (!by.has(it.group)) by.set(it.group, [])\n by.get(it.group)!.push(it)\n }\n\n return [...by.entries()].sort((a, b) => a[0].localeCompare(b[0])).map(([group, items]) => ({ group, items }))\n}\n\n/** 두 뷰를 쓸 수 있는 목록인가 — 호스트가 소속·종류를 주지 않으면 예전처럼 평평하게 보여 준다. */\nexport function hasHierarchy(items: readonly BoardPlacementItem[]): boolean {\n return items.some(i => !!i.parentId)\n}\n\nexport function hasGroups(items: readonly BoardPlacementItem[]): boolean {\n return items.some(i => !!i.group)\n}\n"]}
@@ -3,5 +3,5 @@ import 'regenerator-runtime/runtime';
3
3
  import * as _sceneModule from '@hatiolab/things-scene';
4
4
  // webpack 모듈 네임스페이스를 plain object로 변환하여 window.scene에 노출
5
5
  window.scene = { ..._sceneModule };
6
- import './things-scene-components.import';
6
+ import 'things-scene-components';
7
7
  //# sourceMappingURL=headless-scene-components.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"headless-scene-components.js","sourceRoot":"","sources":["../../client/entries/headless-scene-components.js"],"names":[],"mappings":"AAAA,OAAO,gBAAgB,CAAA;AACvB,OAAO,6BAA6B,CAAA;AACpC,OAAO,KAAK,YAAY,MAAM,wBAAwB,CAAA;AAEtD,yDAAyD;AACzD,MAAM,CAAC,KAAK,GAAG,EAAE,GAAG,YAAY,EAAE,CAAA;AAElC,OAAO,kCAAkC,CAAA","sourcesContent":["import 'core-js/stable'\nimport 'regenerator-runtime/runtime'\nimport * as _sceneModule from '@hatiolab/things-scene'\n\n// webpack 모듈 네임스페이스를 plain object로 변환하여 window.scene에 노출\nwindow.scene = { ..._sceneModule }\n\nimport './things-scene-components.import'\n"]}
1
+ {"version":3,"file":"headless-scene-components.js","sourceRoot":"","sources":["../../client/entries/headless-scene-components.js"],"names":[],"mappings":"AAAA,OAAO,gBAAgB,CAAA;AACvB,OAAO,6BAA6B,CAAA;AACpC,OAAO,KAAK,YAAY,MAAM,wBAAwB,CAAA;AAEtD,yDAAyD;AACzD,MAAM,CAAC,KAAK,GAAG,EAAE,GAAG,YAAY,EAAE,CAAA;AAElC,OAAO,yBAAyB,CAAA","sourcesContent":["import 'core-js/stable'\nimport 'regenerator-runtime/runtime'\nimport * as _sceneModule from '@hatiolab/things-scene'\n\n// webpack 모듈 네임스페이스를 plain object로 변환하여 window.scene에 노출\nwindow.scene = { ..._sceneModule }\n\nimport 'things-scene-components'\n"]}
@@ -1,4 +1,4 @@
1
- import './things-scene-components.import';
1
+ import 'things-scene-components';
2
2
  import '@operato/board/ox-board-modeller.js';
3
3
  import '@operato/oops';
4
4
  import '@things-factory/board-ai';
@@ -1,5 +1,5 @@
1
1
  import { __decorate, __metadata } from "tslib";
2
- import './things-scene-components.import';
2
+ import 'things-scene-components';
3
3
  import '@operato/board/ox-board-modeller.js';
4
4
  import '@operato/oops';
5
5
  import '@things-factory/board-ai';
@@ -18,7 +18,7 @@ import { OxPrompt } from '@operato/popup/ox-prompt.js';
18
18
  import { provider } from '../board-provider.js';
19
19
  import '../board-placement-tray.js'; // 배치 팔레트 오버레이(호스트-등록 provider 있을 때만 표시)
20
20
  import { boardPlacementProvider } from '../board-placement-palette.js';
21
- import components from './things-scene-components-with-tools.import';
21
+ import components from 'things-scene-components-with-tools';
22
22
  import { notify, notifyError } from '../utils/notify-helper.js';
23
23
  import { findSceneComponent, dispatchBoardAction } from './board-action-dispatch.js';
24
24
  import { dispatchBoardEditOp } from './board-edit-dispatch.js';