@operato/property-panel 10.0.0-beta.62 → 10.0.0-beta.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,15 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [10.0.0-beta.63](https://github.com/hatiolab/operato/compare/v10.0.0-beta.62...v10.0.0-beta.63) (2026-07-07)
7
+
8
+
9
+ ### :rocket: New Features
10
+
11
+ * **property-panel:** tree range(volume) select + tighter node spacing ([8443533](https://github.com/hatiolab/operato/commit/8443533425cba514c3aabda971f3ec118ab0d9ac))
12
+
13
+
14
+
6
15
  ## [10.0.0-beta.62](https://github.com/hatiolab/operato/compare/v10.0.0-beta.61...v10.0.0-beta.62) (2026-07-04)
7
16
 
8
17
 
@@ -1,5 +1,5 @@
1
1
  import { Component, Scene } from '@hatiolab/things-scene';
2
- import { InspectorIndex } from './inspector-model.js';
2
+ import { InspectorIndex, SearchTerm } from './inspector-model.js';
3
3
  export declare class InspectorController {
4
4
  private _scene?;
5
5
  private _extendedMap;
@@ -7,6 +7,8 @@ export declare class InspectorController {
7
7
  private _expandedGroups;
8
8
  /** 모델링 포커스 dim 처리된 그룹(타입) 집합 — transient(비영속). */
9
9
  private _dimmedTypes;
10
+ /** shift+click 범위(볼륨) 선택의 기준점 — 마지막 단일/토글 클릭. */
11
+ private _anchor?;
10
12
  private _index?;
11
13
  private _onChange?;
12
14
  /** scene 에 바인딩 — 이전 바인딩은 해제한다. onChange 는 재렌더 요청 콜백. */
@@ -39,6 +41,18 @@ export declare class InspectorController {
39
41
  toggleLocked(component: Component): void;
40
42
  /** 캔버스를 해당 컴포넌트로 센터 이동. */
41
43
  centerTo(component: Component): void;
42
- /** 선택 — append 시 토글식 다중 선택. */
44
+ /** 선택 — append(ctrl/cmd) 시 토글식 다중 선택. anchor 를 갱신한다. */
43
45
  select(component: Component, append?: boolean): void;
46
+ /**
47
+ * shift+click 범위(볼륨) 선택 — anchor↔target 사이의 **보이는 트리 행**을 블록 선택한다.
48
+ * target 이 이미 선택돼 있으면 그 범위를 블록 **해제**(additive/subtractive). anchor 는 유지.
49
+ */
50
+ selectRange(target: Component, terms: SearchTerm[]): void;
51
+ /** 선택 강조. */
52
+ private _decorate;
53
+ /**
54
+ * 현재 트리 뷰에 보이는 컴포넌트를 렌더 순서대로 반환 — 범위 선택의 기준.
55
+ * renderNode 와 동일 규칙(shouldBeShown = 자신/자손 매치, isExtended 자식만 전개).
56
+ */
57
+ private _visibleOrder;
44
58
  }
@@ -1,4 +1,4 @@
1
- import { buildInspectorIndex } from './inspector-model.js';
1
+ import { buildInspectorIndex, matchesQuery } from './inspector-model.js';
2
2
  export class InspectorController {
3
3
  constructor() {
4
4
  this._extendedMap = new WeakMap();
@@ -161,7 +161,7 @@ export class InspectorController {
161
161
  ;
162
162
  (_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.centerTo) === null || _b === void 0 ? void 0 : _b.call(_a, component, true);
163
163
  }
164
- /** 선택 — append 시 토글식 다중 선택. */
164
+ /** 선택 — append(ctrl/cmd) 시 토글식 다중 선택. anchor 를 갱신한다. */
165
165
  select(component, append = false) {
166
166
  var _a;
167
167
  const scene = this._scene;
@@ -181,9 +181,78 @@ export class InspectorController {
181
181
  else {
182
182
  scene.selected = [component];
183
183
  }
184
+ this._anchor = component;
185
+ this._decorate(component);
186
+ (_a = this._onChange) === null || _a === void 0 ? void 0 : _a.call(this);
187
+ }
188
+ /**
189
+ * shift+click 범위(볼륨) 선택 — anchor↔target 사이의 **보이는 트리 행**을 블록 선택한다.
190
+ * target 이 이미 선택돼 있으면 그 범위를 블록 **해제**(additive/subtractive). anchor 는 유지.
191
+ */
192
+ selectRange(target, terms) {
193
+ var _a;
194
+ const scene = this._scene;
195
+ if (!scene)
196
+ return;
197
+ const order = this._visibleOrder(terms);
198
+ const ti = order.indexOf(target);
199
+ if (ti < 0)
200
+ return;
201
+ const ai = this._anchor ? order.indexOf(this._anchor) : -1;
202
+ if (ai < 0) {
203
+ // anchor 없음/보이지 않음 → 단일 토글로 폴백(+ anchor 설정)
204
+ this.select(target, true);
205
+ return;
206
+ }
207
+ const [lo, hi] = ai <= ti ? [ai, ti] : [ti, ai];
208
+ const range = order.slice(lo, hi + 1);
209
+ const rangeSet = new Set(range);
210
+ // target 이 이미 선택 상태면 블록 해제, 아니면 블록 선택.
211
+ if (scene.selected.indexOf(target) > -1) {
212
+ scene.selected = scene.selected.filter((c) => !rangeSet.has(c));
213
+ range.forEach(c => c.trigger('reactionreset'));
214
+ }
215
+ else {
216
+ const cur = new Set(scene.selected);
217
+ const next = [...scene.selected];
218
+ range.forEach(c => {
219
+ if (!cur.has(c))
220
+ next.push(c);
221
+ this._decorate(c);
222
+ });
223
+ scene.selected = next;
224
+ }
225
+ // anchor 는 유지 — 연속 shift+click 으로 범위 확장/축소.
226
+ (_a = this._onChange) === null || _a === void 0 ? void 0 : _a.call(this);
227
+ }
228
+ /** 선택 강조. */
229
+ _decorate(component) {
184
230
  component.trigger('reactionreset');
185
231
  component.trigger('reaction');
186
- (_a = this._onChange) === null || _a === void 0 ? void 0 : _a.call(this);
232
+ }
233
+ /**
234
+ * 현재 트리 뷰에 보이는 컴포넌트를 렌더 순서대로 반환 — 범위 선택의 기준.
235
+ * renderNode 와 동일 규칙(shouldBeShown = 자신/자손 매치, isExtended 자식만 전개).
236
+ */
237
+ _visibleOrder(terms) {
238
+ var _a;
239
+ const root = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root;
240
+ if (!root)
241
+ return [];
242
+ const shown = (c) => matchesQuery(c, terms) ||
243
+ (c.isContainer() && c.components.some((child) => shown(child)));
244
+ const out = [];
245
+ const walk = (c) => {
246
+ if (!shown(c))
247
+ return;
248
+ out.push(c);
249
+ if (this.isExtended(c) && c.isContainer()) {
250
+ for (const child of c.components)
251
+ walk(child);
252
+ }
253
+ };
254
+ walk(root);
255
+ return out;
187
256
  }
188
257
  }
189
258
  //# sourceMappingURL=inspector-controller.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"inspector-controller.js","sourceRoot":"","sources":["../../../../src/property-panel/inspector/inspector-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Scene Inspector 컨트롤러.\n *\n * scene 과의 상호작용(구독·선택·가시성·확장 상태)과 모델 인덱스 캐시를 한 곳에 응집한다.\n * DOM 을 직접 다루지 않으며, 뷰 렌더러(트리/그룹)가 공유한다. 상태 변경 시 onChange 로\n * 호스트(LitElement)의 재렌더를 요청한다.\n */\nimport { Component, Container, Scene } from '@hatiolab/things-scene'\n\nimport { buildInspectorIndex, InspectorIndex } from './inspector-model.js'\n\nexport class InspectorController {\n private _scene?: Scene\n private _extendedMap = new WeakMap<Component, boolean>()\n /** 타입 그룹 뷰의 펼침 상태 — 그룹 키(type)는 안정적이라 Set 으로 관리. 기본 전부 접힘. */\n private _expandedGroups = new Set<string>()\n /** 모델링 포커스 dim 처리된 그룹(타입) 집합 — transient(비영속). */\n private _dimmedTypes = new Set<string>()\n private _index?: InspectorIndex\n private _onChange?: () => void\n\n /** scene 에 바인딩 — 이전 바인딩은 해제한다. onChange 는 재렌더 요청 콜백. */\n bind(scene: Scene | undefined, onChange: () => void): void {\n this.unbind()\n this._scene = scene\n this._onChange = onChange\n this._index = undefined\n\n if (scene && scene.root) {\n scene.on('selected', this._refresh, this)\n scene.on('execute', this._invalidateAndRefresh, this)\n scene.on('undo', this._invalidateAndRefresh, this)\n scene.on('redo', this._invalidateAndRefresh, this)\n this._refresh()\n }\n }\n\n unbind(): void {\n const scene = this._scene\n if (scene) {\n // 포커스 dim 은 transient — 인스펙터가 떠나면 캔버스 dim 해제.\n scene.focusFilter = undefined\n scene.off('selected', undefined, this)\n scene.off('execute', undefined, this)\n scene.off('undo', undefined, this)\n scene.off('redo', undefined, this)\n }\n this._scene = undefined\n this._index = undefined\n this._extendedMap = new WeakMap()\n this._dimmedTypes.clear()\n }\n\n get scene(): Scene | undefined {\n return this._scene\n }\n\n get selected(): Component[] {\n return this._scene?.selected || []\n }\n\n /** 캐시된 모델 인덱스 — 없으면 build. execute/undo/redo 시 무효화. */\n get index(): InspectorIndex | undefined {\n if (!this._scene?.root) return undefined\n if (!this._index) this._index = buildInspectorIndex(this._scene.root)\n return this._index\n }\n\n private _invalidateAndRefresh = (): void => {\n this._index = undefined\n this._refresh()\n }\n\n /** 선택된 컴포넌트의 조상을 펼치고 재렌더 요청. */\n private _refresh = (): void => {\n const selected = this._scene?.selected || []\n selected.forEach((component: Component) => {\n let parent = component.parent\n while (parent && !this._extendedMap.get(parent)) {\n this._extendedMap.set(parent, true)\n parent = parent.parent\n }\n })\n this._onChange?.()\n }\n\n isExtended(component: Component): boolean {\n return !!this._extendedMap.get(component)\n }\n\n toggleExtended(component: Component): void {\n if (this.isExtended(component)) {\n this._extendedMap.delete(component)\n } else {\n this._extendedMap.set(component, true)\n }\n this._onChange?.()\n }\n\n /** 트리 노드 핸들(펼침/접힘/공백) 클래스. */\n nodeHandleClass(component: Component): string {\n if (component.isContainer() && (component as Container).components.length > 0) {\n return this.isExtended(component) ? 'extended' : 'collapsed'\n }\n return 'collapsespace'\n }\n\n // ── 타입 그룹 뷰 확장 상태 ──\n\n isGroupExpanded(type: string): boolean {\n return this._expandedGroups.has(type)\n }\n\n toggleGroup(type: string): void {\n if (this._expandedGroups.has(type)) {\n this._expandedGroups.delete(type)\n } else {\n this._expandedGroups.add(type)\n }\n this._onChange?.()\n }\n\n /** 모든 그룹 접기 — \"그룹 레벨까지만 펼침\"(헤더만 표시). */\n collapseAllGroups(): void {\n if (this._expandedGroups.size === 0) return\n this._expandedGroups.clear()\n this._onChange?.()\n }\n\n /** 모든 그룹 펼치기. */\n expandAllGroups(types: string[]): void {\n types.forEach(t => this._expandedGroups.add(t))\n this._onChange?.()\n }\n\n // ── 모델링 포커스 dim (그룹 단위, transient) ──\n\n isTypeDimmed(type: string): boolean {\n return this._dimmedTypes.has(type)\n }\n\n /** 그룹(타입) dim-out 토글 → scene.focusFilter 재설정(편집 모드에서 캔버스 dim + pick 제외). */\n toggleTypeDim(type: string): void {\n if (this._dimmedTypes.has(type)) this._dimmedTypes.delete(type)\n else this._dimmedTypes.add(type)\n this._applyFocusFilter()\n this._onChange?.()\n }\n\n private _applyFocusFilter(): void {\n if (!this._scene) return\n if (this._dimmedTypes.size === 0) {\n this._scene.focusFilter = undefined\n } else {\n const dimmed = this._dimmedTypes\n // focus(정상) = dim 집합에 없는 타입. 불일치(=dim 그룹)만 캔버스에서 흐림 + pick 제외.\n this._scene.focusFilter = (component: Component) => !dimmed.has(component.state.type)\n }\n }\n\n /** 개별 컴포넌트 hidden(영구 모델 속성) 토글 — 기존 eye 동작. */\n toggleHidden(component: Component): void {\n component.set('hidden', !component.get('hidden'))\n this._onChange?.()\n }\n\n /** 개별 컴포넌트 locked(영구 모델 속성) 토글. */\n toggleLocked(component: Component): void {\n component.set('locked', !component.get('locked'))\n this._onChange?.()\n }\n\n /** 캔버스를 해당 컴포넌트로 센터 이동. */\n centerTo(component: Component): void {\n ;(this._scene as unknown as { centerTo?: (c: Component, animated?: boolean) => void })?.centerTo?.(component, true)\n }\n\n /** 선택 — append 시 토글식 다중 선택. */\n select(component: Component, append = false): void {\n const scene = this._scene\n if (!scene) return\n\n const selected = scene.selected\n if (append) {\n const idx = selected.findIndex((s: Component) => s === component)\n if (idx != -1) {\n selected.splice(idx, 1)\n scene.selected = [...selected]\n } else {\n scene.selected = [...selected, component]\n }\n } else {\n scene.selected = [component]\n }\n\n component.trigger('reactionreset')\n component.trigger('reaction')\n\n this._onChange?.()\n }\n}\n"]}
1
+ {"version":3,"file":"inspector-controller.js","sourceRoot":"","sources":["../../../../src/property-panel/inspector/inspector-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Scene Inspector 컨트롤러.\n *\n * scene 과의 상호작용(구독·선택·가시성·확장 상태)과 모델 인덱스 캐시를 한 곳에 응집한다.\n * DOM 을 직접 다루지 않으며, 뷰 렌더러(트리/그룹)가 공유한다. 상태 변경 시 onChange 로\n * 호스트(LitElement)의 재렌더를 요청한다.\n */\nimport { Component, Container, Scene } from '@hatiolab/things-scene'\n\nimport { buildInspectorIndex, InspectorIndex, matchesQuery, SearchTerm } from './inspector-model.js'\n\nexport class InspectorController {\n private _scene?: Scene\n private _extendedMap = new WeakMap<Component, boolean>()\n /** 타입 그룹 뷰의 펼침 상태 — 그룹 키(type)는 안정적이라 Set 으로 관리. 기본 전부 접힘. */\n private _expandedGroups = new Set<string>()\n /** 모델링 포커스 dim 처리된 그룹(타입) 집합 — transient(비영속). */\n private _dimmedTypes = new Set<string>()\n /** shift+click 범위(볼륨) 선택의 기준점 — 마지막 단일/토글 클릭. */\n private _anchor?: Component\n private _index?: InspectorIndex\n private _onChange?: () => void\n\n /** scene 에 바인딩 — 이전 바인딩은 해제한다. onChange 는 재렌더 요청 콜백. */\n bind(scene: Scene | undefined, onChange: () => void): void {\n this.unbind()\n this._scene = scene\n this._onChange = onChange\n this._index = undefined\n\n if (scene && scene.root) {\n scene.on('selected', this._refresh, this)\n scene.on('execute', this._invalidateAndRefresh, this)\n scene.on('undo', this._invalidateAndRefresh, this)\n scene.on('redo', this._invalidateAndRefresh, this)\n this._refresh()\n }\n }\n\n unbind(): void {\n const scene = this._scene\n if (scene) {\n // 포커스 dim 은 transient — 인스펙터가 떠나면 캔버스 dim 해제.\n scene.focusFilter = undefined\n scene.off('selected', undefined, this)\n scene.off('execute', undefined, this)\n scene.off('undo', undefined, this)\n scene.off('redo', undefined, this)\n }\n this._scene = undefined\n this._index = undefined\n this._extendedMap = new WeakMap()\n this._dimmedTypes.clear()\n }\n\n get scene(): Scene | undefined {\n return this._scene\n }\n\n get selected(): Component[] {\n return this._scene?.selected || []\n }\n\n /** 캐시된 모델 인덱스 — 없으면 build. execute/undo/redo 시 무효화. */\n get index(): InspectorIndex | undefined {\n if (!this._scene?.root) return undefined\n if (!this._index) this._index = buildInspectorIndex(this._scene.root)\n return this._index\n }\n\n private _invalidateAndRefresh = (): void => {\n this._index = undefined\n this._refresh()\n }\n\n /** 선택된 컴포넌트의 조상을 펼치고 재렌더 요청. */\n private _refresh = (): void => {\n const selected = this._scene?.selected || []\n selected.forEach((component: Component) => {\n let parent = component.parent\n while (parent && !this._extendedMap.get(parent)) {\n this._extendedMap.set(parent, true)\n parent = parent.parent\n }\n })\n this._onChange?.()\n }\n\n isExtended(component: Component): boolean {\n return !!this._extendedMap.get(component)\n }\n\n toggleExtended(component: Component): void {\n if (this.isExtended(component)) {\n this._extendedMap.delete(component)\n } else {\n this._extendedMap.set(component, true)\n }\n this._onChange?.()\n }\n\n /** 트리 노드 핸들(펼침/접힘/공백) 클래스. */\n nodeHandleClass(component: Component): string {\n if (component.isContainer() && (component as Container).components.length > 0) {\n return this.isExtended(component) ? 'extended' : 'collapsed'\n }\n return 'collapsespace'\n }\n\n // ── 타입 그룹 뷰 확장 상태 ──\n\n isGroupExpanded(type: string): boolean {\n return this._expandedGroups.has(type)\n }\n\n toggleGroup(type: string): void {\n if (this._expandedGroups.has(type)) {\n this._expandedGroups.delete(type)\n } else {\n this._expandedGroups.add(type)\n }\n this._onChange?.()\n }\n\n /** 모든 그룹 접기 — \"그룹 레벨까지만 펼침\"(헤더만 표시). */\n collapseAllGroups(): void {\n if (this._expandedGroups.size === 0) return\n this._expandedGroups.clear()\n this._onChange?.()\n }\n\n /** 모든 그룹 펼치기. */\n expandAllGroups(types: string[]): void {\n types.forEach(t => this._expandedGroups.add(t))\n this._onChange?.()\n }\n\n // ── 모델링 포커스 dim (그룹 단위, transient) ──\n\n isTypeDimmed(type: string): boolean {\n return this._dimmedTypes.has(type)\n }\n\n /** 그룹(타입) dim-out 토글 → scene.focusFilter 재설정(편집 모드에서 캔버스 dim + pick 제외). */\n toggleTypeDim(type: string): void {\n if (this._dimmedTypes.has(type)) this._dimmedTypes.delete(type)\n else this._dimmedTypes.add(type)\n this._applyFocusFilter()\n this._onChange?.()\n }\n\n private _applyFocusFilter(): void {\n if (!this._scene) return\n if (this._dimmedTypes.size === 0) {\n this._scene.focusFilter = undefined\n } else {\n const dimmed = this._dimmedTypes\n // focus(정상) = dim 집합에 없는 타입. 불일치(=dim 그룹)만 캔버스에서 흐림 + pick 제외.\n this._scene.focusFilter = (component: Component) => !dimmed.has(component.state.type)\n }\n }\n\n /** 개별 컴포넌트 hidden(영구 모델 속성) 토글 — 기존 eye 동작. */\n toggleHidden(component: Component): void {\n component.set('hidden', !component.get('hidden'))\n this._onChange?.()\n }\n\n /** 개별 컴포넌트 locked(영구 모델 속성) 토글. */\n toggleLocked(component: Component): void {\n component.set('locked', !component.get('locked'))\n this._onChange?.()\n }\n\n /** 캔버스를 해당 컴포넌트로 센터 이동. */\n centerTo(component: Component): void {\n ;(this._scene as unknown as { centerTo?: (c: Component, animated?: boolean) => void })?.centerTo?.(component, true)\n }\n\n /** 선택 — append(ctrl/cmd) 시 토글식 다중 선택. anchor 를 갱신한다. */\n select(component: Component, append = false): void {\n const scene = this._scene\n if (!scene) return\n\n const selected = scene.selected\n if (append) {\n const idx = selected.findIndex((s: Component) => s === component)\n if (idx != -1) {\n selected.splice(idx, 1)\n scene.selected = [...selected]\n } else {\n scene.selected = [...selected, component]\n }\n } else {\n scene.selected = [component]\n }\n\n this._anchor = component\n this._decorate(component)\n this._onChange?.()\n }\n\n /**\n * shift+click 범위(볼륨) 선택 — anchor↔target 사이의 **보이는 트리 행**을 블록 선택한다.\n * target 이 이미 선택돼 있으면 그 범위를 블록 **해제**(additive/subtractive). anchor 는 유지.\n */\n selectRange(target: Component, terms: SearchTerm[]): void {\n const scene = this._scene\n if (!scene) return\n\n const order = this._visibleOrder(terms)\n const ti = order.indexOf(target)\n if (ti < 0) return\n\n const ai = this._anchor ? order.indexOf(this._anchor) : -1\n if (ai < 0) {\n // anchor 없음/보이지 않음 → 단일 토글로 폴백(+ anchor 설정)\n this.select(target, true)\n return\n }\n\n const [lo, hi] = ai <= ti ? [ai, ti] : [ti, ai]\n const range = order.slice(lo, hi + 1)\n const rangeSet = new Set(range)\n\n // target 이 이미 선택 상태면 블록 해제, 아니면 블록 선택.\n if (scene.selected.indexOf(target) > -1) {\n scene.selected = scene.selected.filter((c: Component) => !rangeSet.has(c))\n range.forEach(c => c.trigger('reactionreset'))\n } else {\n const cur = new Set(scene.selected)\n const next = [...scene.selected]\n range.forEach(c => {\n if (!cur.has(c)) next.push(c)\n this._decorate(c)\n })\n scene.selected = next\n }\n // anchor 는 유지 — 연속 shift+click 으로 범위 확장/축소.\n this._onChange?.()\n }\n\n /** 선택 강조. */\n private _decorate(component: Component): void {\n component.trigger('reactionreset')\n component.trigger('reaction')\n }\n\n /**\n * 현재 트리 뷰에 보이는 컴포넌트를 렌더 순서대로 반환 — 범위 선택의 기준.\n * renderNode 와 동일 규칙(shouldBeShown = 자신/자손 매치, isExtended 자식만 전개).\n */\n private _visibleOrder(terms: SearchTerm[]): Component[] {\n const root = this._scene?.root\n if (!root) return []\n\n const shown = (c: Component): boolean =>\n matchesQuery(c, terms) ||\n (c.isContainer() && (c as Container).components.some((child: Component) => shown(child)))\n\n const out: Component[] = []\n const walk = (c: Component) => {\n if (!shown(c)) return\n out.push(c)\n if (this.isExtended(c) && c.isContainer()) {\n for (const child of (c as Container).components) walk(child)\n }\n }\n walk(root)\n return out\n }\n}\n"]}
@@ -38,9 +38,9 @@ function renderNode(component, depth, ctx) {
38
38
  <md-icon class="lock" ?locked=${!!component.get('locked')}
39
39
  >${component.get('locked') ? 'lock' : 'lock_open'}</md-icon
40
40
  >
41
- <pre>${' '.repeat(depth)}</pre>
41
+ <pre>${' '.repeat(Math.max(0, depth - 1))}</pre>
42
42
  `
43
- : html ` <pre>${' '.repeat(depth + 2)}</pre> `}
43
+ : html ` <pre>${' '.repeat(depth + 1)}</pre> `}
44
44
 
45
45
  <span class=${ctx.nodeHandleClass(component)}> </span>
46
46
 
@@ -1 +1 @@
1
- {"version":3,"file":"inspector-tree-view.js","sourceRoot":"","sources":["../../../../src/property-panel/inspector/inspector-tree-view.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Scene Inspector 트리 뷰 렌더러.\n *\n * 계층(Container/Group) 구조를 재귀로 그린다. SceneInspector 의 shadow root 에 직접 렌더되며\n * (클릭 위임이 `.component` 프로퍼티/classList 에 의존하므로 별도 커스텀엘리먼트로 분리하지 않는다),\n * 순수 렌더 함수로 컨트롤러/상태를 주입받는다.\n */\nimport { Component, Container } from '@hatiolab/things-scene'\nimport { html, nothing, TemplateResult } from 'lit'\n\nimport { matchesQuery, SearchTerm } from './inspector-model.js'\nimport { highlight } from './inspector-highlight.js'\n\n/** 트리 렌더에 필요한 컨텍스트(호스트/컨트롤러가 주입). */\nexport interface TreeViewContext {\n /** 현재 선택 컴포넌트 목록. */\n selected: Component[]\n /** 파싱된 검색어(AND). */\n terms: SearchTerm[]\n /** ROOT 노드에 표시할 매치 개수. */\n count: number\n isExtended: (c: Component) => boolean\n nodeHandleClass: (c: Component) => string\n}\n\n/** 컴포넌트 자신 또는 자손 중 검색 매치가 있으면 표시 대상. */\nfunction shouldBeShown(component: Component, terms: SearchTerm[]): boolean {\n if (matchesQuery(component, terms)) return true\n const children = component.isContainer() ? (component as Container).components : []\n return children.some(child => shouldBeShown(child, terms))\n}\n\n/** 검색 매치 개수(루트 포함, 전체 트리) — ROOT 노드 count 표시용. */\nexport function countMatches(component: Component, terms: SearchTerm[]): number {\n let n = matchesQuery(component, terms) ? 1 : 0\n const children = component.isContainer() ? (component as Container).components : []\n for (const child of children) n += countMatches(child, terms)\n return n\n}\n\nfunction renderNode(component: Component, depth: number, ctx: TreeViewContext): TemplateResult {\n if (!component || !shouldBeShown(component, ctx.terms)) {\n return html``\n }\n\n const children = component.isContainer() ? (component as Container).components : []\n const extended = ctx.isExtended(component) ? children.filter(child => shouldBeShown(child, ctx.terms)) : []\n const { type, id, tag, class: clazz } = component.state\n const label = (id ? `#${id}` : '') + (tag ? `@${tag}` : '') + (clazz ? `.(${clazz})` : '')\n\n return html`\n <div\n class=\"component\"\n ?selected=${ctx.selected.indexOf(component) > -1}\n .component=${component}\n ?sortable=${component.isContainer()}\n >\n <span>\n ${depth > 0\n ? html`\n <md-icon class=\"eye\">${component.get('hidden') ? 'visibility_off' : 'visibility'}</md-icon>\n <md-icon class=\"lock\" ?locked=${!!component.get('locked')}\n >${component.get('locked') ? 'lock' : 'lock_open'}</md-icon\n >\n <pre>${' '.repeat(depth)}</pre>\n `\n : html` <pre>${' '.repeat(depth + 2)}</pre> `}\n\n <span class=${ctx.nodeHandleClass(component)}> </span>\n\n <span class=\"type\">${depth == 0 ? html`ROOT(count: ${ctx.count})` : highlight(type, ctx.terms)}</span>\n ${label ? html` <span class=\"name\">${highlight(label, ctx.terms)}</span> ` : nothing}\n </span>\n\n ${extended.map(child => renderNode(child, depth + 1, ctx))}\n </div>\n `\n}\n\n/** scene.root 부터 트리를 렌더한다. */\nexport function renderInspectorTree(root: Component | undefined, ctx: TreeViewContext): TemplateResult {\n if (!root) return html``\n return renderNode(root, 0, ctx)\n}\n"]}
1
+ {"version":3,"file":"inspector-tree-view.js","sourceRoot":"","sources":["../../../../src/property-panel/inspector/inspector-tree-view.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Scene Inspector 트리 뷰 렌더러.\n *\n * 계층(Container/Group) 구조를 재귀로 그린다. SceneInspector 의 shadow root 에 직접 렌더되며\n * (클릭 위임이 `.component` 프로퍼티/classList 에 의존하므로 별도 커스텀엘리먼트로 분리하지 않는다),\n * 순수 렌더 함수로 컨트롤러/상태를 주입받는다.\n */\nimport { Component, Container } from '@hatiolab/things-scene'\nimport { html, nothing, TemplateResult } from 'lit'\n\nimport { matchesQuery, SearchTerm } from './inspector-model.js'\nimport { highlight } from './inspector-highlight.js'\n\n/** 트리 렌더에 필요한 컨텍스트(호스트/컨트롤러가 주입). */\nexport interface TreeViewContext {\n /** 현재 선택 컴포넌트 목록. */\n selected: Component[]\n /** 파싱된 검색어(AND). */\n terms: SearchTerm[]\n /** ROOT 노드에 표시할 매치 개수. */\n count: number\n isExtended: (c: Component) => boolean\n nodeHandleClass: (c: Component) => string\n}\n\n/** 컴포넌트 자신 또는 자손 중 검색 매치가 있으면 표시 대상. */\nfunction shouldBeShown(component: Component, terms: SearchTerm[]): boolean {\n if (matchesQuery(component, terms)) return true\n const children = component.isContainer() ? (component as Container).components : []\n return children.some(child => shouldBeShown(child, terms))\n}\n\n/** 검색 매치 개수(루트 포함, 전체 트리) — ROOT 노드 count 표시용. */\nexport function countMatches(component: Component, terms: SearchTerm[]): number {\n let n = matchesQuery(component, terms) ? 1 : 0\n const children = component.isContainer() ? (component as Container).components : []\n for (const child of children) n += countMatches(child, terms)\n return n\n}\n\nfunction renderNode(component: Component, depth: number, ctx: TreeViewContext): TemplateResult {\n if (!component || !shouldBeShown(component, ctx.terms)) {\n return html``\n }\n\n const children = component.isContainer() ? (component as Container).components : []\n const extended = ctx.isExtended(component) ? children.filter(child => shouldBeShown(child, ctx.terms)) : []\n const { type, id, tag, class: clazz } = component.state\n const label = (id ? `#${id}` : '') + (tag ? `@${tag}` : '') + (clazz ? `.(${clazz})` : '')\n\n return html`\n <div\n class=\"component\"\n ?selected=${ctx.selected.indexOf(component) > -1}\n .component=${component}\n ?sortable=${component.isContainer()}\n >\n <span>\n ${depth > 0\n ? html`\n <md-icon class=\"eye\">${component.get('hidden') ? 'visibility_off' : 'visibility'}</md-icon>\n <md-icon class=\"lock\" ?locked=${!!component.get('locked')}\n >${component.get('locked') ? 'lock' : 'lock_open'}</md-icon\n >\n <pre>${' '.repeat(Math.max(0, depth - 1))}</pre>\n `\n : html` <pre>${' '.repeat(depth + 1)}</pre> `}\n\n <span class=${ctx.nodeHandleClass(component)}> </span>\n\n <span class=\"type\">${depth == 0 ? html`ROOT(count: ${ctx.count})` : highlight(type, ctx.terms)}</span>\n ${label ? html` <span class=\"name\">${highlight(label, ctx.terms)}</span> ` : nothing}\n </span>\n\n ${extended.map(child => renderNode(child, depth + 1, ctx))}\n </div>\n `\n}\n\n/** scene.root 부터 트리를 렌더한다. */\nexport function renderInspectorTree(root: Component | undefined, ctx: TreeViewContext): TemplateResult {\n if (!root) return html``\n return renderNode(root, 0, ctx)\n}\n"]}
@@ -196,7 +196,13 @@ export class SceneInspector extends LitElement {
196
196
  else if (classList.contains('extended') || classList.contains('collapsed')) {
197
197
  this.controller.toggleExtended(component);
198
198
  }
199
- this.controller.select(component, e.shiftKey);
199
+ // shift = 범위(볼륨) 선택/블록 해제, ctrl/cmd = 개별 토글, 그 외 = 단일 선택
200
+ if (e.shiftKey) {
201
+ this.controller.selectRange(component, parseQuery(this.searchText));
202
+ }
203
+ else {
204
+ this.controller.select(component, e.metaKey || e.ctrlKey);
205
+ }
200
206
  }
201
207
  else {
202
208
  if (classList.contains('inspector')) {
@@ -284,19 +290,21 @@ SceneInspector.styles = [
284
290
  }
285
291
 
286
292
  .eye {
287
- margin: 0 0 0 5px;
293
+ margin: 0 0 0 4px;
288
294
  vertical-align: middle;
289
295
  opacity: 0.7;
290
- font-size: 1.1em;
296
+ font-size: 1em;
297
+ width: 1em;
291
298
  color: var(--scene-inspector-eye-icon-color);
292
299
  cursor: pointer;
293
300
  }
294
301
 
295
302
  .lock {
296
- margin: 0 0 0 2px;
303
+ margin: 0;
297
304
  vertical-align: middle;
298
305
  opacity: 0.3;
299
306
  font-size: 1em;
307
+ width: 1em;
300
308
  cursor: pointer;
301
309
  }
302
310
  .lock[locked] {
@@ -306,6 +314,9 @@ SceneInspector.styles = [
306
314
 
307
315
  pre {
308
316
  display: inline;
317
+ /* 트리 인덴트(수직라인까지의 간격) 최소화 — depth 당 좁은 폭 */
318
+ font-size: 0.6em;
319
+ margin: 0;
309
320
  }
310
321
 
311
322
  /* 뷰 모드 툴바 */
@@ -1 +1 @@
1
- {"version":3,"file":"inspector.js","sourceRoot":"","sources":["../../../../src/property-panel/inspector/inspector.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * SceneInspector — 속성패널 inspector 탭 호스트.\n *\n * 얇은 호스트: 검색 상태 + 클릭 위임만 담당하고, scene 상호작용은 InspectorController,\n * 렌더는 뷰 렌더러(현재 트리)로 위임한다. (아키텍처 리팩터 P0 — 동작은 기존과 동일)\n */\nimport '@operato/input/ox-input-search.js'\n\nimport { css, html, LitElement, nothing, PropertyValues, TemplateResult } from 'lit'\nimport { property, state } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\n\nimport { Component, Scene } from '@hatiolab/things-scene'\nimport { InspectorStyles } from '@operato/styles/inspector-styles.js'\n\nimport { parseQuery } from './inspector-model.js'\nimport { InspectorController } from './inspector-controller.js'\nimport { countMatches, renderInspectorTree } from './inspector-tree-view.js'\nimport { renderInspectorGroupView } from './inspector-group-view.js'\n\ntype InspectorViewMode = 'tree' | 'group'\n\n/** 뷰 모드 선호를 세션 간 유지(localStorage). 전역 사용자 선호. */\nconst VIEW_MODE_STORAGE_KEY = 'scene-inspector.view-mode'\n\nfunction readStoredViewMode(): InspectorViewMode {\n try {\n const v = localStorage.getItem(VIEW_MODE_STORAGE_KEY)\n return v === 'group' || v === 'tree' ? v : 'tree'\n } catch {\n return 'tree'\n }\n}\n\nfunction writeStoredViewMode(mode: InspectorViewMode): void {\n try {\n localStorage.setItem(VIEW_MODE_STORAGE_KEY, mode)\n } catch {\n /* localStorage 미가용(SSR/프라이빗 등) — 무시 */\n }\n}\n\nexport class SceneInspector extends LitElement {\n static styles = [\n InspectorStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n\n color: var(--scene-inspector-color);\n }\n\n ox-input-search {\n --input-search-padding: 3px;\n --input-search-focus-border-bottom: none;\n --input-search-font: normal 16px var(--theme-font);\n\n border-radius: 999px;\n box-sizing: border-box;\n border: 1px solid var(--md-sys-color-on-secondary-container);\n padding: 0 10px;\n margin: 5px;\n }\n\n div[result] {\n flex: 1;\n\n display: flex;\n flex-direction: column;\n\n overflow-y: auto;\n }\n\n .component {\n display: block;\n overflow: hidden;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n font-size: 14px;\n }\n\n .component[selected] {\n background-color: var(--scene-inspector-selected-background-color);\n border-top: var(--scene-inspector-selected-border);\n border-bottom: var(--scene-inspector-selected-border);\n }\n [selected] .type {\n font-weight: bold;\n }\n\n span,\n i {\n display: inline-block;\n }\n\n span.type {\n text-overflow: ellipses;\n }\n\n span.name {\n background-color: var(--scene-inspector-name-background-color);\n border-radius: var(--border-radius);\n padding: 0 4px;\n color: #fff;\n font-size: 0.8em;\n }\n\n .eye {\n margin: 0 0 0 5px;\n vertical-align: middle;\n opacity: 0.7;\n font-size: 1.1em;\n color: var(--scene-inspector-eye-icon-color);\n cursor: pointer;\n }\n\n .lock {\n margin: 0 0 0 2px;\n vertical-align: middle;\n opacity: 0.3;\n font-size: 1em;\n cursor: pointer;\n }\n .lock[locked] {\n opacity: 0.9;\n color: var(--md-sys-color-error, #d9534f);\n }\n\n pre {\n display: inline;\n }\n\n /* 뷰 모드 툴바 */\n div[toolbar] {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px 8px 4px;\n }\n div[toolbar] .spacer {\n flex: 1;\n }\n div[toolbar] md-icon {\n cursor: pointer;\n opacity: 0.5;\n font-size: 18px;\n border-radius: 4px;\n padding: 2px;\n }\n div[toolbar] md-icon:hover {\n opacity: 0.9;\n }\n div[toolbar] md-icon[selected] {\n opacity: 1;\n color: var(--md-sys-color-primary, #4a9eff);\n background: rgba(0, 0, 0, 0.05);\n }\n\n /* 타입 그룹 뷰 */\n .group-header {\n display: flex;\n align-items: center;\n gap: 4px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n padding: 2px 0;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n }\n .group-header:hover {\n background: rgba(0, 0, 0, 0.03);\n }\n .group-count {\n margin-left: auto;\n font-size: 0.72em;\n font-weight: normal;\n background: var(--scene-inspector-name-background-color);\n color: #fff;\n border-radius: 999px;\n padding: 0 6px;\n }\n .group-dim {\n margin: 0 4px 0 6px;\n font-size: 1em;\n opacity: 0.35;\n cursor: pointer;\n }\n .group-dim:hover {\n opacity: 0.8;\n }\n .group-header[dimmed] .group-dim {\n opacity: 0.95;\n color: var(--md-sys-color-primary, #4a9eff);\n }\n .group-header[dimmed] .group-type {\n opacity: 0.45;\n }\n .group-member {\n padding-left: 8px;\n }\n .muted {\n opacity: 0.4;\n font-style: italic;\n }\n /* 검색 매치 하이라이트 */\n mark {\n background: var(--md-sys-color-tertiary-container, #ffe082);\n color: inherit;\n border-radius: 2px;\n padding: 0 1px;\n }\n `\n ]\n\n @property({ type: Object }) scene?: Scene\n\n @state() private searchText: string = ''\n @state() private viewMode: InspectorViewMode = readStoredViewMode()\n\n private controller = new InspectorController()\n private show: boolean = false\n /** reveal(스크롤) 판정용 직전 선택. */\n private _lastSelected: Component[] = []\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.controller.unbind()\n }\n\n render() {\n return html`\n <ox-input-search\n .placeholder=${i18next.t('text.search with type, id or tag')}\n autofocus\n @change=${(e: Event) => (this.searchText = ((e.target as HTMLInputElement).value || '').toLowerCase())}\n ></ox-input-search>\n\n <div toolbar>\n <md-icon class=\"view-btn\" ?selected=${this.viewMode === 'tree'} title=\"Tree view\" @click=${() => this._setViewMode('tree')}>account_tree</md-icon>\n <md-icon class=\"view-btn\" ?selected=${this.viewMode === 'group'} title=\"Group by type\" @click=${() => this._setViewMode('group')}>category</md-icon>\n <span class=\"spacer\"></span>\n ${this.viewMode === 'group'\n ? html`\n <md-icon title=\"Expand all groups\" @click=${this._expandGroups}>unfold_more</md-icon>\n <md-icon title=\"Collapse to group level\" @click=${this._collapseGroups}>unfold_less</md-icon>\n `\n : nothing}\n </div>\n\n <div result>${this._renderResult()}</div>\n `\n }\n\n private _renderResult(): TemplateResult {\n if (!this.scene) return html``\n\n const terms = parseQuery(this.searchText)\n\n if (this.viewMode === 'group') {\n return renderInspectorGroupView(this.controller.index, {\n selected: this.controller.selected,\n terms,\n isGroupExpanded: (type: string) => this.controller.isGroupExpanded(type),\n isTypeDimmed: (type: string) => this.controller.isTypeDimmed(type)\n })\n }\n\n return renderInspectorTree(this.scene.root, {\n selected: this.controller.selected,\n terms,\n count: countMatches(this.scene.root, terms),\n isExtended: (c: Component) => this.controller.isExtended(c),\n nodeHandleClass: (c: Component) => this.controller.nodeHandleClass(c)\n })\n }\n\n private _setViewMode(mode: InspectorViewMode) {\n if (this.viewMode === mode) return\n this.viewMode = mode\n writeStoredViewMode(mode) // 세션 간 유지\n }\n\n private _collapseGroups = () => {\n this.controller.collapseAllGroups()\n this.requestUpdate()\n }\n\n private _expandGroups = () => {\n const index = this.controller.index\n if (index) this.controller.expandAllGroups([...index.byType.keys()])\n this.requestUpdate()\n }\n\n firstUpdated() {\n dispatchEvent(new Event('resize'))\n\n this.renderRoot.addEventListener('click', this._onclick.bind(this) as EventListener)\n this.renderRoot.addEventListener('dblclick', this._ondblclick.bind(this) as EventListener)\n }\n\n /** 외부에서 강제 재렌더용(기존 공개 메쏘드 유지). */\n refresh() {\n this.requestUpdate()\n }\n\n updated(change: PropertyValues<this>) {\n if (change.has('scene')) {\n this.controller.bind(this.scene, () => this.requestUpdate())\n }\n this._maybeReveal()\n }\n\n /**\n * 선택이 바뀌면(캔버스 등) 해당 항목을 리스트에 노출한다 — 그룹 뷰면 그룹을 펼친 뒤 스크롤.\n * scrollIntoView({block:'nearest'}) 라 이미 보이는 경우(인스펙터 클릭 등)엔 사실상 no-op.\n */\n private _maybeReveal() {\n const selected = this.controller.selected\n const changed =\n selected.length !== this._lastSelected.length || selected.some((c, i) => c !== this._lastSelected[i])\n if (!changed) return\n\n if (selected.length === 0) {\n this._lastSelected = selected\n return\n }\n\n if (this.viewMode === 'group') {\n const types = [...new Set(selected.map(c => c.state.type))]\n if (types.some(t => !this.controller.isGroupExpanded(t))) {\n this.controller.expandAllGroups(types)\n return // _lastSelected 미갱신 → 다음 updated 에서 changed 유지 → 그때 스크롤\n }\n }\n\n this._lastSelected = selected\n this.updateComplete.then(() => {\n const el = this.renderRoot.querySelector('.component[selected]') as HTMLElement | null\n el?.scrollIntoView({ block: 'nearest' })\n })\n }\n\n /** 이벤트 타겟에서 상위로 올라가며 `.component` 를 가진 요소를 찾는다. */\n private _resolveComponent(e: MouseEvent): Component | undefined {\n let targetElement = e.target as HTMLElement & { component: Component }\n let component: Component | undefined\n\n while (!component && targetElement) {\n component = targetElement.component\n\n if (component) break\n\n targetElement = targetElement.parentNode as HTMLElement & { component: Component }\n\n if (!targetElement || targetElement === this.renderRoot) break\n }\n\n return component\n }\n\n _onclick(e: MouseEvent) {\n e.stopPropagation()\n\n const target = e.target as HTMLElement\n\n // 그룹 dim-out 아이콘 클릭 → 그룹(타입) 포커스 dim 토글 (헤더 펼침보다 먼저 처리)\n if (target.classList.contains('group-dim')) {\n const type = target.closest?.('[data-group-type]')?.getAttribute('data-group-type')\n if (type != null) this.controller.toggleTypeDim(type)\n this.requestUpdate()\n return\n }\n\n // 타입 그룹 헤더 클릭 → 그룹 펼침/접힘 (멤버 행은 헤더의 자손이 아니므로 여기 안 걸림)\n const header = target.closest?.('[data-group-type]') as HTMLElement | null\n if (header) {\n const type = header.getAttribute('data-group-type')\n if (type != null) this.controller.toggleGroup(type)\n this.requestUpdate()\n return\n }\n\n const classList = (e.target as HTMLElement).classList\n const component = this._resolveComponent(e)\n\n if (component) {\n if (classList.contains('eye')) {\n this.controller.toggleHidden(component)\n } else if (classList.contains('lock')) {\n this.controller.toggleLocked(component)\n } else if (classList.contains('extended') || classList.contains('collapsed')) {\n this.controller.toggleExtended(component)\n }\n\n this.controller.select(component, e.shiftKey)\n } else {\n if (classList.contains('inspector')) {\n this.show = !this.show\n this.style.height = this.show ? '100%' : ''\n }\n }\n\n this.requestUpdate()\n }\n\n _ondblclick(e: MouseEvent) {\n e.stopPropagation()\n\n const component = this._resolveComponent(e)\n\n if (component) {\n if (component.isContainer()) {\n this.controller.toggleExtended(component)\n }\n // 캔버스를 해당 컴포넌트로 센터 이동(줌).\n this.controller.centerTo(component)\n }\n\n this.requestUpdate()\n }\n}\n"]}
1
+ {"version":3,"file":"inspector.js","sourceRoot":"","sources":["../../../../src/property-panel/inspector/inspector.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * SceneInspector — 속성패널 inspector 탭 호스트.\n *\n * 얇은 호스트: 검색 상태 + 클릭 위임만 담당하고, scene 상호작용은 InspectorController,\n * 렌더는 뷰 렌더러(현재 트리)로 위임한다. (아키텍처 리팩터 P0 — 동작은 기존과 동일)\n */\nimport '@operato/input/ox-input-search.js'\n\nimport { css, html, LitElement, nothing, PropertyValues, TemplateResult } from 'lit'\nimport { property, state } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\n\nimport { Component, Scene } from '@hatiolab/things-scene'\nimport { InspectorStyles } from '@operato/styles/inspector-styles.js'\n\nimport { parseQuery } from './inspector-model.js'\nimport { InspectorController } from './inspector-controller.js'\nimport { countMatches, renderInspectorTree } from './inspector-tree-view.js'\nimport { renderInspectorGroupView } from './inspector-group-view.js'\n\ntype InspectorViewMode = 'tree' | 'group'\n\n/** 뷰 모드 선호를 세션 간 유지(localStorage). 전역 사용자 선호. */\nconst VIEW_MODE_STORAGE_KEY = 'scene-inspector.view-mode'\n\nfunction readStoredViewMode(): InspectorViewMode {\n try {\n const v = localStorage.getItem(VIEW_MODE_STORAGE_KEY)\n return v === 'group' || v === 'tree' ? v : 'tree'\n } catch {\n return 'tree'\n }\n}\n\nfunction writeStoredViewMode(mode: InspectorViewMode): void {\n try {\n localStorage.setItem(VIEW_MODE_STORAGE_KEY, mode)\n } catch {\n /* localStorage 미가용(SSR/프라이빗 등) — 무시 */\n }\n}\n\nexport class SceneInspector extends LitElement {\n static styles = [\n InspectorStyles,\n css`\n :host {\n display: flex;\n flex-direction: column;\n\n color: var(--scene-inspector-color);\n }\n\n ox-input-search {\n --input-search-padding: 3px;\n --input-search-focus-border-bottom: none;\n --input-search-font: normal 16px var(--theme-font);\n\n border-radius: 999px;\n box-sizing: border-box;\n border: 1px solid var(--md-sys-color-on-secondary-container);\n padding: 0 10px;\n margin: 5px;\n }\n\n div[result] {\n flex: 1;\n\n display: flex;\n flex-direction: column;\n\n overflow-y: auto;\n }\n\n .component {\n display: block;\n overflow: hidden;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n font-size: 14px;\n }\n\n .component[selected] {\n background-color: var(--scene-inspector-selected-background-color);\n border-top: var(--scene-inspector-selected-border);\n border-bottom: var(--scene-inspector-selected-border);\n }\n [selected] .type {\n font-weight: bold;\n }\n\n span,\n i {\n display: inline-block;\n }\n\n span.type {\n text-overflow: ellipses;\n }\n\n span.name {\n background-color: var(--scene-inspector-name-background-color);\n border-radius: var(--border-radius);\n padding: 0 4px;\n color: #fff;\n font-size: 0.8em;\n }\n\n .eye {\n margin: 0 0 0 4px;\n vertical-align: middle;\n opacity: 0.7;\n font-size: 1em;\n width: 1em;\n color: var(--scene-inspector-eye-icon-color);\n cursor: pointer;\n }\n\n .lock {\n margin: 0;\n vertical-align: middle;\n opacity: 0.3;\n font-size: 1em;\n width: 1em;\n cursor: pointer;\n }\n .lock[locked] {\n opacity: 0.9;\n color: var(--md-sys-color-error, #d9534f);\n }\n\n pre {\n display: inline;\n /* 트리 인덴트(수직라인까지의 간격) 최소화 — depth 당 좁은 폭 */\n font-size: 0.6em;\n margin: 0;\n }\n\n /* 뷰 모드 툴바 */\n div[toolbar] {\n display: flex;\n align-items: center;\n gap: 4px;\n padding: 2px 8px 4px;\n }\n div[toolbar] .spacer {\n flex: 1;\n }\n div[toolbar] md-icon {\n cursor: pointer;\n opacity: 0.5;\n font-size: 18px;\n border-radius: 4px;\n padding: 2px;\n }\n div[toolbar] md-icon:hover {\n opacity: 0.9;\n }\n div[toolbar] md-icon[selected] {\n opacity: 1;\n color: var(--md-sys-color-primary, #4a9eff);\n background: rgba(0, 0, 0, 0.05);\n }\n\n /* 타입 그룹 뷰 */\n .group-header {\n display: flex;\n align-items: center;\n gap: 4px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n padding: 2px 0;\n border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n }\n .group-header:hover {\n background: rgba(0, 0, 0, 0.03);\n }\n .group-count {\n margin-left: auto;\n font-size: 0.72em;\n font-weight: normal;\n background: var(--scene-inspector-name-background-color);\n color: #fff;\n border-radius: 999px;\n padding: 0 6px;\n }\n .group-dim {\n margin: 0 4px 0 6px;\n font-size: 1em;\n opacity: 0.35;\n cursor: pointer;\n }\n .group-dim:hover {\n opacity: 0.8;\n }\n .group-header[dimmed] .group-dim {\n opacity: 0.95;\n color: var(--md-sys-color-primary, #4a9eff);\n }\n .group-header[dimmed] .group-type {\n opacity: 0.45;\n }\n .group-member {\n padding-left: 8px;\n }\n .muted {\n opacity: 0.4;\n font-style: italic;\n }\n /* 검색 매치 하이라이트 */\n mark {\n background: var(--md-sys-color-tertiary-container, #ffe082);\n color: inherit;\n border-radius: 2px;\n padding: 0 1px;\n }\n `\n ]\n\n @property({ type: Object }) scene?: Scene\n\n @state() private searchText: string = ''\n @state() private viewMode: InspectorViewMode = readStoredViewMode()\n\n private controller = new InspectorController()\n private show: boolean = false\n /** reveal(스크롤) 판정용 직전 선택. */\n private _lastSelected: Component[] = []\n\n disconnectedCallback() {\n super.disconnectedCallback()\n this.controller.unbind()\n }\n\n render() {\n return html`\n <ox-input-search\n .placeholder=${i18next.t('text.search with type, id or tag')}\n autofocus\n @change=${(e: Event) => (this.searchText = ((e.target as HTMLInputElement).value || '').toLowerCase())}\n ></ox-input-search>\n\n <div toolbar>\n <md-icon class=\"view-btn\" ?selected=${this.viewMode === 'tree'} title=\"Tree view\" @click=${() => this._setViewMode('tree')}>account_tree</md-icon>\n <md-icon class=\"view-btn\" ?selected=${this.viewMode === 'group'} title=\"Group by type\" @click=${() => this._setViewMode('group')}>category</md-icon>\n <span class=\"spacer\"></span>\n ${this.viewMode === 'group'\n ? html`\n <md-icon title=\"Expand all groups\" @click=${this._expandGroups}>unfold_more</md-icon>\n <md-icon title=\"Collapse to group level\" @click=${this._collapseGroups}>unfold_less</md-icon>\n `\n : nothing}\n </div>\n\n <div result>${this._renderResult()}</div>\n `\n }\n\n private _renderResult(): TemplateResult {\n if (!this.scene) return html``\n\n const terms = parseQuery(this.searchText)\n\n if (this.viewMode === 'group') {\n return renderInspectorGroupView(this.controller.index, {\n selected: this.controller.selected,\n terms,\n isGroupExpanded: (type: string) => this.controller.isGroupExpanded(type),\n isTypeDimmed: (type: string) => this.controller.isTypeDimmed(type)\n })\n }\n\n return renderInspectorTree(this.scene.root, {\n selected: this.controller.selected,\n terms,\n count: countMatches(this.scene.root, terms),\n isExtended: (c: Component) => this.controller.isExtended(c),\n nodeHandleClass: (c: Component) => this.controller.nodeHandleClass(c)\n })\n }\n\n private _setViewMode(mode: InspectorViewMode) {\n if (this.viewMode === mode) return\n this.viewMode = mode\n writeStoredViewMode(mode) // 세션 간 유지\n }\n\n private _collapseGroups = () => {\n this.controller.collapseAllGroups()\n this.requestUpdate()\n }\n\n private _expandGroups = () => {\n const index = this.controller.index\n if (index) this.controller.expandAllGroups([...index.byType.keys()])\n this.requestUpdate()\n }\n\n firstUpdated() {\n dispatchEvent(new Event('resize'))\n\n this.renderRoot.addEventListener('click', this._onclick.bind(this) as EventListener)\n this.renderRoot.addEventListener('dblclick', this._ondblclick.bind(this) as EventListener)\n }\n\n /** 외부에서 강제 재렌더용(기존 공개 메쏘드 유지). */\n refresh() {\n this.requestUpdate()\n }\n\n updated(change: PropertyValues<this>) {\n if (change.has('scene')) {\n this.controller.bind(this.scene, () => this.requestUpdate())\n }\n this._maybeReveal()\n }\n\n /**\n * 선택이 바뀌면(캔버스 등) 해당 항목을 리스트에 노출한다 — 그룹 뷰면 그룹을 펼친 뒤 스크롤.\n * scrollIntoView({block:'nearest'}) 라 이미 보이는 경우(인스펙터 클릭 등)엔 사실상 no-op.\n */\n private _maybeReveal() {\n const selected = this.controller.selected\n const changed =\n selected.length !== this._lastSelected.length || selected.some((c, i) => c !== this._lastSelected[i])\n if (!changed) return\n\n if (selected.length === 0) {\n this._lastSelected = selected\n return\n }\n\n if (this.viewMode === 'group') {\n const types = [...new Set(selected.map(c => c.state.type))]\n if (types.some(t => !this.controller.isGroupExpanded(t))) {\n this.controller.expandAllGroups(types)\n return // _lastSelected 미갱신 → 다음 updated 에서 changed 유지 → 그때 스크롤\n }\n }\n\n this._lastSelected = selected\n this.updateComplete.then(() => {\n const el = this.renderRoot.querySelector('.component[selected]') as HTMLElement | null\n el?.scrollIntoView({ block: 'nearest' })\n })\n }\n\n /** 이벤트 타겟에서 상위로 올라가며 `.component` 를 가진 요소를 찾는다. */\n private _resolveComponent(e: MouseEvent): Component | undefined {\n let targetElement = e.target as HTMLElement & { component: Component }\n let component: Component | undefined\n\n while (!component && targetElement) {\n component = targetElement.component\n\n if (component) break\n\n targetElement = targetElement.parentNode as HTMLElement & { component: Component }\n\n if (!targetElement || targetElement === this.renderRoot) break\n }\n\n return component\n }\n\n _onclick(e: MouseEvent) {\n e.stopPropagation()\n\n const target = e.target as HTMLElement\n\n // 그룹 dim-out 아이콘 클릭 → 그룹(타입) 포커스 dim 토글 (헤더 펼침보다 먼저 처리)\n if (target.classList.contains('group-dim')) {\n const type = target.closest?.('[data-group-type]')?.getAttribute('data-group-type')\n if (type != null) this.controller.toggleTypeDim(type)\n this.requestUpdate()\n return\n }\n\n // 타입 그룹 헤더 클릭 → 그룹 펼침/접힘 (멤버 행은 헤더의 자손이 아니므로 여기 안 걸림)\n const header = target.closest?.('[data-group-type]') as HTMLElement | null\n if (header) {\n const type = header.getAttribute('data-group-type')\n if (type != null) this.controller.toggleGroup(type)\n this.requestUpdate()\n return\n }\n\n const classList = (e.target as HTMLElement).classList\n const component = this._resolveComponent(e)\n\n if (component) {\n if (classList.contains('eye')) {\n this.controller.toggleHidden(component)\n } else if (classList.contains('lock')) {\n this.controller.toggleLocked(component)\n } else if (classList.contains('extended') || classList.contains('collapsed')) {\n this.controller.toggleExtended(component)\n }\n\n // shift = 범위(볼륨) 선택/블록 해제, ctrl/cmd = 개별 토글, 그 외 = 단일 선택\n if (e.shiftKey) {\n this.controller.selectRange(component, parseQuery(this.searchText))\n } else {\n this.controller.select(component, e.metaKey || e.ctrlKey)\n }\n } else {\n if (classList.contains('inspector')) {\n this.show = !this.show\n this.style.height = this.show ? '100%' : ''\n }\n }\n\n this.requestUpdate()\n }\n\n _ondblclick(e: MouseEvent) {\n e.stopPropagation()\n\n const component = this._resolveComponent(e)\n\n if (component) {\n if (component.isContainer()) {\n this.controller.toggleExtended(component)\n }\n // 캔버스를 해당 컴포넌트로 센터 이동(줌).\n this.controller.centerTo(component)\n }\n\n this.requestUpdate()\n }\n}\n"]}