@operato/property-panel 10.0.0-beta.7 → 10.0.0

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.
Files changed (84) hide show
  1. package/CHANGELOG.md +478 -0
  2. package/dist/src/graphql/data-subscription.js +5 -1
  3. package/dist/src/graphql/data-subscription.js.map +1 -1
  4. package/dist/src/index.d.ts +4 -0
  5. package/dist/src/index.js +6 -0
  6. package/dist/src/index.js.map +1 -1
  7. package/dist/src/ox-property-panel.d.ts +17 -0
  8. package/dist/src/ox-property-panel.js +105 -39
  9. package/dist/src/ox-property-panel.js.map +1 -1
  10. package/dist/src/property-panel/abstract-property.js +11 -2
  11. package/dist/src/property-panel/abstract-property.js.map +1 -1
  12. package/dist/src/property-panel/data-binding/data-binding-mapper.js +11 -3
  13. package/dist/src/property-panel/data-binding/data-binding-mapper.js.map +1 -1
  14. package/dist/src/property-panel/data-binding/data-binding-popup.d.ts +63 -0
  15. package/dist/src/property-panel/data-binding/data-binding-popup.js +1414 -0
  16. package/dist/src/property-panel/data-binding/data-binding-popup.js.map +1 -0
  17. package/dist/src/property-panel/data-binding/data-binding.d.ts +1 -0
  18. package/dist/src/property-panel/data-binding/data-binding.js +37 -5
  19. package/dist/src/property-panel/data-binding/data-binding.js.map +1 -1
  20. package/dist/src/property-panel/effects/effects.js +1 -1
  21. package/dist/src/property-panel/effects/effects.js.map +1 -1
  22. package/dist/src/property-panel/effects/property-event-tap.js +21 -1
  23. package/dist/src/property-panel/effects/property-event-tap.js.map +1 -1
  24. package/dist/src/property-panel/effects/property-event.d.ts +15 -7
  25. package/dist/src/property-panel/effects/property-event.js +17 -38
  26. package/dist/src/property-panel/effects/property-event.js.map +1 -1
  27. package/dist/src/property-panel/effects/property-shadow.js +14 -5
  28. package/dist/src/property-panel/effects/property-shadow.js.map +1 -1
  29. package/dist/src/property-panel/event-handlers/event-handlers-help.d.ts +23 -0
  30. package/dist/src/property-panel/event-handlers/event-handlers-help.js +356 -0
  31. package/dist/src/property-panel/event-handlers/event-handlers-help.js.map +1 -0
  32. package/dist/src/property-panel/event-handlers/event-handlers-mapper.d.ts +31 -0
  33. package/dist/src/property-panel/event-handlers/event-handlers-mapper.js +238 -0
  34. package/dist/src/property-panel/event-handlers/event-handlers-mapper.js.map +1 -0
  35. package/dist/src/property-panel/event-handlers/event-handlers-popup.d.ts +42 -0
  36. package/dist/src/property-panel/event-handlers/event-handlers-popup.js +375 -0
  37. package/dist/src/property-panel/event-handlers/event-handlers-popup.js.map +1 -0
  38. package/dist/src/property-panel/event-handlers/event-handlers.d.ts +54 -0
  39. package/dist/src/property-panel/event-handlers/event-handlers.js +410 -0
  40. package/dist/src/property-panel/event-handlers/event-handlers.js.map +1 -0
  41. package/dist/src/property-panel/inspector/inspector-controller.d.ts +66 -0
  42. package/dist/src/property-panel/inspector/inspector-controller.js +288 -0
  43. package/dist/src/property-panel/inspector/inspector-controller.js.map +1 -0
  44. package/dist/src/property-panel/inspector/inspector-group-view.d.ts +15 -0
  45. package/dist/src/property-panel/inspector/inspector-group-view.js +59 -0
  46. package/dist/src/property-panel/inspector/inspector-group-view.js.map +1 -0
  47. package/dist/src/property-panel/inspector/inspector-highlight.d.ts +7 -0
  48. package/dist/src/property-panel/inspector/inspector-highlight.js +25 -0
  49. package/dist/src/property-panel/inspector/inspector-highlight.js.map +1 -0
  50. package/dist/src/property-panel/inspector/inspector-model.d.ts +44 -0
  51. package/dist/src/property-panel/inspector/inspector-model.js +72 -0
  52. package/dist/src/property-panel/inspector/inspector-model.js.map +1 -0
  53. package/dist/src/property-panel/inspector/inspector-tree-view.d.ts +18 -0
  54. package/dist/src/property-panel/inspector/inspector-tree-view.js +61 -0
  55. package/dist/src/property-panel/inspector/inspector-tree-view.js.map +1 -0
  56. package/dist/src/property-panel/inspector/inspector.d.ts +17 -12
  57. package/dist/src/property-panel/inspector/inspector.js +281 -179
  58. package/dist/src/property-panel/inspector/inspector.js.map +1 -1
  59. package/dist/src/property-panel/shapes/ox-placeholder-convert-editor.d.ts +20 -0
  60. package/dist/src/property-panel/shapes/ox-placeholder-convert-editor.js +125 -0
  61. package/dist/src/property-panel/shapes/ox-placeholder-convert-editor.js.map +1 -0
  62. package/dist/src/property-panel/shapes/placeholder-convert.d.ts +34 -0
  63. package/dist/src/property-panel/shapes/placeholder-convert.js +117 -0
  64. package/dist/src/property-panel/shapes/placeholder-convert.js.map +1 -0
  65. package/dist/src/property-panel/shapes/shapes.js +28 -10
  66. package/dist/src/property-panel/shapes/shapes.js.map +1 -1
  67. package/dist/src/property-panel/specifics/specifics.d.ts +1 -0
  68. package/dist/src/property-panel/specifics/specifics.js +2 -1
  69. package/dist/src/property-panel/specifics/specifics.js.map +1 -1
  70. package/dist/src/property-panel/threed/property-material3d.js +8 -1
  71. package/dist/src/property-panel/threed/property-material3d.js.map +1 -1
  72. package/dist/src/property-panel/threed/property-scene3d.d.ts +34 -6
  73. package/dist/src/property-panel/threed/property-scene3d.js +332 -201
  74. package/dist/src/property-panel/threed/property-scene3d.js.map +1 -1
  75. package/dist/src/property-panel/threed/threed.js +8 -0
  76. package/dist/src/property-panel/threed/threed.js.map +1 -1
  77. package/dist/tsconfig.tsbuildinfo +1 -1
  78. package/package.json +8 -7
  79. package/themes/grist-theme.css +1 -1
  80. package/translations/en.json +34 -1
  81. package/translations/ja.json +34 -1
  82. package/translations/ko.json +34 -1
  83. package/translations/ms.json +34 -1
  84. package/translations/zh.json +34 -1
@@ -0,0 +1,288 @@
1
+ import { buildInspectorIndex, matchesQuery } from './inspector-model.js';
2
+ export class InspectorController {
3
+ constructor() {
4
+ this._extendedMap = new WeakMap();
5
+ /** 타입 그룹 뷰의 펼침 상태 — 그룹 키(type)는 안정적이라 Set 으로 관리. 기본 전부 접힘. */
6
+ this._expandedGroups = new Set();
7
+ /**
8
+ * 모델링 포커스 dim 처리된 그룹(타입) 집합.
9
+ * scene 에 보존(scene._inspectorDimmedTypes)하여 인스펙터 탭을 벗어났다 돌아와도 유지된다.
10
+ * (보드를 다시 열면 새 scene 이라 자연 초기화 — transient.) bind 에서 scene 것으로 재취득.
11
+ */
12
+ this._dimmedTypes = new Set();
13
+ this._invalidateAndRefresh = () => {
14
+ this._index = undefined;
15
+ this._refresh();
16
+ };
17
+ /** 선택된 컴포넌트의 조상을 펼치고 재렌더 요청. */
18
+ this._refresh = () => {
19
+ var _a, _b;
20
+ const selected = ((_a = this._scene) === null || _a === void 0 ? void 0 : _a.selected) || [];
21
+ selected.forEach((component) => {
22
+ let parent = component.parent;
23
+ while (parent && !this._extendedMap.get(parent)) {
24
+ this._extendedMap.set(parent, true);
25
+ parent = parent.parent;
26
+ }
27
+ });
28
+ (_b = this._onChange) === null || _b === void 0 ? void 0 : _b.call(this);
29
+ };
30
+ }
31
+ /** scene 에 바인딩 — 이전 바인딩은 해제한다. onChange 는 재렌더 요청 콜백. */
32
+ bind(scene, onChange) {
33
+ this.unbind();
34
+ this._scene = scene;
35
+ this._onChange = onChange;
36
+ this._index = undefined;
37
+ if (scene && scene.root) {
38
+ // dim 상태는 scene 에 보존 — 인스펙터 탭을 벗어났다 돌아와도(재마운트) 유지된다.
39
+ const anyScene = scene;
40
+ if (!anyScene._inspectorDimmedTypes)
41
+ anyScene._inspectorDimmedTypes = new Set();
42
+ this._dimmedTypes = anyScene._inspectorDimmedTypes;
43
+ this._applyFocusFilter(); // 보존된 dim 을 캔버스에 재적용 (idempotent)
44
+ scene.on('selected', this._refresh, this);
45
+ scene.on('execute', this._invalidateAndRefresh, this);
46
+ scene.on('undo', this._invalidateAndRefresh, this);
47
+ scene.on('redo', this._invalidateAndRefresh, this);
48
+ this._refresh();
49
+ }
50
+ }
51
+ unbind() {
52
+ const scene = this._scene;
53
+ if (scene) {
54
+ // dim(포커스 필터)은 편집 세션 동안 유지 — 인스펙터 탭을 벗어나도 해제하지 않는다.
55
+ // scene.focusFilter 와 scene._inspectorDimmedTypes 는 그대로 두고, 이벤트 구독만 해제.
56
+ // (보드를 다시 열면 새 scene 이라 자연 초기화된다.)
57
+ scene.off('selected', undefined, this);
58
+ scene.off('execute', undefined, this);
59
+ scene.off('undo', undefined, this);
60
+ scene.off('redo', undefined, this);
61
+ }
62
+ this._scene = undefined;
63
+ this._index = undefined;
64
+ this._extendedMap = new WeakMap();
65
+ // _dimmedTypes 는 scene 에 보존되므로 지우지 않는다. 로컬 참조만 분리(다음 bind 에서 재취득).
66
+ this._dimmedTypes = new Set();
67
+ }
68
+ get scene() {
69
+ return this._scene;
70
+ }
71
+ get selected() {
72
+ var _a;
73
+ return ((_a = this._scene) === null || _a === void 0 ? void 0 : _a.selected) || [];
74
+ }
75
+ /** 캐시된 모델 인덱스 — 없으면 build. execute/undo/redo 시 무효화. */
76
+ get index() {
77
+ var _a;
78
+ if (!((_a = this._scene) === null || _a === void 0 ? void 0 : _a.root))
79
+ return undefined;
80
+ if (!this._index)
81
+ this._index = buildInspectorIndex(this._scene.root);
82
+ return this._index;
83
+ }
84
+ isExtended(component) {
85
+ return !!this._extendedMap.get(component);
86
+ }
87
+ toggleExtended(component) {
88
+ var _a;
89
+ if (this.isExtended(component)) {
90
+ this._extendedMap.delete(component);
91
+ }
92
+ else {
93
+ this._extendedMap.set(component, true);
94
+ }
95
+ (_a = this._onChange) === null || _a === void 0 ? void 0 : _a.call(this);
96
+ }
97
+ /** 트리 노드 핸들(펼침/접힘/공백) 클래스. */
98
+ nodeHandleClass(component) {
99
+ if (component.isContainer() && component.components.length > 0) {
100
+ return this.isExtended(component) ? 'extended' : 'collapsed';
101
+ }
102
+ return 'collapsespace';
103
+ }
104
+ // ── 타입 그룹 뷰 확장 상태 ──
105
+ isGroupExpanded(type) {
106
+ return this._expandedGroups.has(type);
107
+ }
108
+ toggleGroup(type) {
109
+ var _a;
110
+ if (this._expandedGroups.has(type)) {
111
+ this._expandedGroups.delete(type);
112
+ }
113
+ else {
114
+ this._expandedGroups.add(type);
115
+ }
116
+ (_a = this._onChange) === null || _a === void 0 ? void 0 : _a.call(this);
117
+ }
118
+ /** 모든 그룹 접기 — "그룹 레벨까지만 펼침"(헤더만 표시). */
119
+ collapseAllGroups() {
120
+ var _a;
121
+ if (this._expandedGroups.size === 0)
122
+ return;
123
+ this._expandedGroups.clear();
124
+ (_a = this._onChange) === null || _a === void 0 ? void 0 : _a.call(this);
125
+ }
126
+ /** 모든 그룹 펼치기. */
127
+ expandAllGroups(types) {
128
+ var _a;
129
+ types.forEach(t => this._expandedGroups.add(t));
130
+ (_a = this._onChange) === null || _a === void 0 ? void 0 : _a.call(this);
131
+ }
132
+ // ── 모델링 포커스 dim (그룹 단위, transient) ──
133
+ isTypeDimmed(type) {
134
+ return this._dimmedTypes.has(type);
135
+ }
136
+ /** 그룹(타입) dim-out 토글 → scene.focusFilter 재설정(편집 모드에서 캔버스 dim + pick 제외). */
137
+ toggleTypeDim(type) {
138
+ var _a;
139
+ if (this._dimmedTypes.has(type))
140
+ this._dimmedTypes.delete(type);
141
+ else
142
+ this._dimmedTypes.add(type);
143
+ this._applyFocusFilter();
144
+ (_a = this._onChange) === null || _a === void 0 ? void 0 : _a.call(this);
145
+ }
146
+ _applyFocusFilter() {
147
+ if (!this._scene)
148
+ return;
149
+ if (this._dimmedTypes.size === 0) {
150
+ this._scene.focusFilter = undefined;
151
+ }
152
+ else {
153
+ const dimmed = this._dimmedTypes;
154
+ // focus(정상) = dim 집합에 없는 타입. 불일치(=dim 그룹)만 캔버스에서 흐림 + pick 제외.
155
+ this._scene.focusFilter = (component) => !dimmed.has(component.state.type);
156
+ }
157
+ }
158
+ // ── 그룹(타입) 단위 hidden 토글 (개별 hidden 의 집계 — dim 과 달리 영구 모델 속성) ──
159
+ /** 그룹 전원이 hidden 이면 true(집계). 멤버/트리 eye 규약과 동일한 아이콘 판정에 사용. */
160
+ isTypeHidden(type) {
161
+ var _a;
162
+ const entries = (_a = this._index) === null || _a === void 0 ? void 0 : _a.byType.get(type);
163
+ return !!entries && entries.length > 0 && entries.every(e => !!e.component.get('hidden'));
164
+ }
165
+ /** 그룹(타입) hidden 토글 → 하나라도 보이면 전원 숨김, 전원 숨김이면 전원 표시. */
166
+ toggleTypeHidden(type) {
167
+ var _a, _b;
168
+ const entries = (_a = this._index) === null || _a === void 0 ? void 0 : _a.byType.get(type);
169
+ if (!entries || entries.length === 0)
170
+ return;
171
+ const target = !entries.every(e => !!e.component.get('hidden'));
172
+ for (const e of entries)
173
+ e.component.set('hidden', target);
174
+ (_b = this._onChange) === null || _b === void 0 ? void 0 : _b.call(this);
175
+ }
176
+ /** 개별 컴포넌트 hidden(영구 모델 속성) 토글 — 기존 eye 동작. */
177
+ toggleHidden(component) {
178
+ var _a;
179
+ component.set('hidden', !component.get('hidden'));
180
+ (_a = this._onChange) === null || _a === void 0 ? void 0 : _a.call(this);
181
+ }
182
+ /** 개별 컴포넌트 locked(영구 모델 속성) 토글. */
183
+ toggleLocked(component) {
184
+ var _a;
185
+ component.set('locked', !component.get('locked'));
186
+ (_a = this._onChange) === null || _a === void 0 ? void 0 : _a.call(this);
187
+ }
188
+ /** 캔버스를 해당 컴포넌트로 센터 이동. */
189
+ centerTo(component) {
190
+ var _a, _b;
191
+ ;
192
+ (_b = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.centerTo) === null || _b === void 0 ? void 0 : _b.call(_a, component, true);
193
+ }
194
+ /** 선택 — append(ctrl/cmd) 시 토글식 다중 선택. anchor 를 갱신한다. */
195
+ select(component, append = false) {
196
+ var _a;
197
+ const scene = this._scene;
198
+ if (!scene)
199
+ return;
200
+ const selected = scene.selected;
201
+ if (append) {
202
+ const idx = selected.findIndex((s) => s === component);
203
+ if (idx != -1) {
204
+ selected.splice(idx, 1);
205
+ scene.selected = [...selected];
206
+ }
207
+ else {
208
+ scene.selected = [...selected, component];
209
+ }
210
+ }
211
+ else {
212
+ scene.selected = [component];
213
+ }
214
+ this._anchor = component;
215
+ this._decorate(component);
216
+ (_a = this._onChange) === null || _a === void 0 ? void 0 : _a.call(this);
217
+ }
218
+ /**
219
+ * shift+click 범위(볼륨) 선택 — anchor↔target 사이의 **보이는 트리 행**을 블록 선택한다.
220
+ * target 이 이미 선택돼 있으면 그 범위를 블록 **해제**(additive/subtractive). anchor 는 유지.
221
+ */
222
+ selectRange(target, terms) {
223
+ var _a;
224
+ const scene = this._scene;
225
+ if (!scene)
226
+ return;
227
+ const order = this._visibleOrder(terms);
228
+ const ti = order.indexOf(target);
229
+ if (ti < 0)
230
+ return;
231
+ const ai = this._anchor ? order.indexOf(this._anchor) : -1;
232
+ if (ai < 0) {
233
+ // anchor 없음/보이지 않음 → 단일 토글로 폴백(+ anchor 설정)
234
+ this.select(target, true);
235
+ return;
236
+ }
237
+ const [lo, hi] = ai <= ti ? [ai, ti] : [ti, ai];
238
+ const range = order.slice(lo, hi + 1);
239
+ const rangeSet = new Set(range);
240
+ // target 이 이미 선택 상태면 블록 해제, 아니면 블록 선택.
241
+ if (scene.selected.indexOf(target) > -1) {
242
+ scene.selected = scene.selected.filter((c) => !rangeSet.has(c));
243
+ range.forEach(c => c.trigger('reactionreset'));
244
+ }
245
+ else {
246
+ const cur = new Set(scene.selected);
247
+ const next = [...scene.selected];
248
+ range.forEach(c => {
249
+ if (!cur.has(c))
250
+ next.push(c);
251
+ this._decorate(c);
252
+ });
253
+ scene.selected = next;
254
+ }
255
+ // anchor 는 유지 — 연속 shift+click 으로 범위 확장/축소.
256
+ (_a = this._onChange) === null || _a === void 0 ? void 0 : _a.call(this);
257
+ }
258
+ /** 선택 강조. */
259
+ _decorate(component) {
260
+ component.trigger('reactionreset');
261
+ component.trigger('reaction');
262
+ }
263
+ /**
264
+ * 현재 트리 뷰에 보이는 컴포넌트를 렌더 순서대로 반환 — 범위 선택의 기준.
265
+ * renderNode 와 동일 규칙(shouldBeShown = 자신/자손 매치, isExtended 자식만 전개).
266
+ */
267
+ _visibleOrder(terms) {
268
+ var _a;
269
+ const root = (_a = this._scene) === null || _a === void 0 ? void 0 : _a.root;
270
+ if (!root)
271
+ return [];
272
+ const shown = (c) => matchesQuery(c, terms) ||
273
+ (c.isContainer() && c.components.some((child) => shown(child)));
274
+ const out = [];
275
+ const walk = (c) => {
276
+ if (!shown(c))
277
+ return;
278
+ out.push(c);
279
+ if (this.isExtended(c) && c.isContainer()) {
280
+ for (const child of c.components)
281
+ walk(child);
282
+ }
283
+ };
284
+ walk(root);
285
+ return out;
286
+ }
287
+ }
288
+ //# sourceMappingURL=inspector-controller.js.map
@@ -0,0 +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, 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 /**\n * 모델링 포커스 dim 처리된 그룹(타입) 집합.\n * scene 에 보존(scene._inspectorDimmedTypes)하여 인스펙터 탭을 벗어났다 돌아와도 유지된다.\n * (보드를 다시 열면 새 scene 이라 자연 초기화 — transient.) bind 에서 scene 것으로 재취득.\n */\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 // dim 상태는 scene 에 보존 — 인스펙터 탭을 벗어났다 돌아와도(재마운트) 유지된다.\n const anyScene = scene as any\n if (!anyScene._inspectorDimmedTypes) anyScene._inspectorDimmedTypes = new Set<string>()\n this._dimmedTypes = anyScene._inspectorDimmedTypes\n this._applyFocusFilter() // 보존된 dim 을 캔버스에 재적용 (idempotent)\n\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(포커스 필터)은 편집 세션 동안 유지 — 인스펙터 탭을 벗어나도 해제하지 않는다.\n // scene.focusFilter 와 scene._inspectorDimmedTypes 는 그대로 두고, 이벤트 구독만 해제.\n // (보드를 다시 열면 새 scene 이라 자연 초기화된다.)\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 // _dimmedTypes 는 scene 에 보존되므로 지우지 않는다. 로컬 참조만 분리(다음 bind 에서 재취득).\n this._dimmedTypes = new Set<string>()\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 토글 (개별 hidden 의 집계 — dim 과 달리 영구 모델 속성) ──\n\n /** 그룹 전원이 hidden 이면 true(집계). 멤버/트리 eye 규약과 동일한 아이콘 판정에 사용. */\n isTypeHidden(type: string): boolean {\n const entries = this._index?.byType.get(type)\n return !!entries && entries.length > 0 && entries.every(e => !!e.component.get('hidden'))\n }\n\n /** 그룹(타입) hidden 토글 → 하나라도 보이면 전원 숨김, 전원 숨김이면 전원 표시. */\n toggleTypeHidden(type: string): void {\n const entries = this._index?.byType.get(type)\n if (!entries || entries.length === 0) return\n const target = !entries.every(e => !!e.component.get('hidden'))\n for (const e of entries) e.component.set('hidden', target)\n this._onChange?.()\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"]}
@@ -0,0 +1,15 @@
1
+ import { Component } from '@hatiolab/things-scene';
2
+ import { TemplateResult } from 'lit';
3
+ import { InspectorIndex, SearchTerm } from './inspector-model.js';
4
+ export interface GroupViewContext {
5
+ selected: Component[];
6
+ /** 파싱된 전역 검색어(AND). */
7
+ terms: SearchTerm[];
8
+ isGroupExpanded: (type: string) => boolean;
9
+ /** 그룹(타입)이 모델링 포커스 dim 처리됐는지. */
10
+ isTypeDimmed: (type: string) => boolean;
11
+ /** 그룹(타입) 전원이 hidden(숨김)인지 — 그룹 hidden 토글 아이콘 판정. */
12
+ isTypeHidden: (type: string) => boolean;
13
+ }
14
+ /** 타입 그룹 뷰 전체 렌더 — 타입명 오름차순. */
15
+ export declare function renderInspectorGroupView(index: InspectorIndex | undefined, ctx: GroupViewContext): TemplateResult;
@@ -0,0 +1,59 @@
1
+ import { html, nothing } from 'lit';
2
+ import { matchesQuery } from './inspector-model.js';
3
+ import { highlight } from './inspector-highlight.js';
4
+ function renderMember(entry, terms, selected) {
5
+ const component = entry.component;
6
+ return html `
7
+ <div class="component group-member" ?selected=${selected.indexOf(component) > -1} .component=${component}>
8
+ <span>
9
+ <md-icon class="eye">${component.get('hidden') ? 'visibility_off' : 'visibility'}</md-icon>
10
+ <md-icon class="lock" ?locked=${!!component.get('locked')}
11
+ >${component.get('locked') ? 'lock' : 'lock_open'}</md-icon
12
+ >
13
+ <pre>${' '}</pre>
14
+ ${entry.label
15
+ ? html `<span class="name">${highlight(entry.label, terms)}</span>`
16
+ : html `<span class="muted">(no id)</span>`}
17
+ </span>
18
+ </div>
19
+ `;
20
+ }
21
+ function renderGroup(type, entries, ctx) {
22
+ // 전역 검색으로 멤버를 좁히고, 매치 없는 그룹은 숨긴다.
23
+ const members = ctx.terms.length ? entries.filter(e => matchesQuery(e.component, ctx.terms)) : entries;
24
+ if (ctx.terms.length && members.length === 0)
25
+ return html ``;
26
+ const expanded = ctx.isGroupExpanded(type);
27
+ return html `
28
+ <div class="group">
29
+ <div
30
+ class="group-header"
31
+ data-group-type=${type}
32
+ ?dimmed=${ctx.isTypeDimmed(type)}
33
+ ?hidden-group=${ctx.isTypeHidden(type)}
34
+ >
35
+ <span class=${expanded ? 'extended' : 'collapsed'}> </span>
36
+ <span class="group-type">${highlight(type, ctx.terms)}</span>
37
+ <span class="group-count">${members.length}</span>
38
+ <md-icon class="group-hidden" title="Show/hide this group"
39
+ >${ctx.isTypeHidden(type) ? 'visibility_off' : 'visibility'}</md-icon
40
+ >
41
+ ${type === 'group'
42
+ ? // 'group'(순수 컨테이너)은 dim 이 무의미 — 자식이 각자 다른 type 이라 캔버스에서 안 흐려짐.
43
+ nothing
44
+ : html `<md-icon class="group-dim" title="Dim this group in canvas (modeling focus)"
45
+ >${ctx.isTypeDimmed(type) ? 'blur_on' : 'blur_off'}</md-icon
46
+ >`}
47
+ </div>
48
+ ${expanded ? members.map(entry => renderMember(entry, ctx.terms, ctx.selected)) : nothing}
49
+ </div>
50
+ `;
51
+ }
52
+ /** 타입 그룹 뷰 전체 렌더 — 타입명 오름차순. */
53
+ export function renderInspectorGroupView(index, ctx) {
54
+ if (!index)
55
+ return html ``;
56
+ const types = [...index.byType.keys()].sort((a, b) => a.localeCompare(b));
57
+ return html `${types.map(type => renderGroup(type, index.byType.get(type), ctx))}`;
58
+ }
59
+ //# sourceMappingURL=inspector-group-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspector-group-view.js","sourceRoot":"","sources":["../../../../src/property-panel/inspector/inspector-group-view.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Scene Inspector 타입 그룹 뷰 렌더러.\n *\n * 계층을 무시하고 `state.type` 기준 **논리 버킷**(= \"컴포넌트 그룹\")으로 재편성해 렌더한다.\n * 그룹 헤더(타입 + 개수 + 펼침/접힘 + dim-out 토글)와 멤버 행(개별 컴포넌트)으로 구성.\n * SceneInspector shadow root 에 직접 렌더 — 멤버 행은 트리 뷰와 동일한 `.component`/eye 규약을\n * 재사용해 기존 클릭 위임(선택·hidden·lock 토글)을 그대로 쓴다. 그룹 헤더 토글은 `data-group-type` 로 처리.\n *\n * (그룹별 필터는 전역 검색의 필드 토큰 `type:...` 과 중복이라 제거됨.)\n */\nimport { Component } from '@hatiolab/things-scene'\nimport { html, nothing, TemplateResult } from 'lit'\n\nimport { IndexEntry, InspectorIndex, matchesQuery, SearchTerm } from './inspector-model.js'\nimport { highlight } from './inspector-highlight.js'\n\nexport interface GroupViewContext {\n selected: Component[]\n /** 파싱된 전역 검색어(AND). */\n terms: SearchTerm[]\n isGroupExpanded: (type: string) => boolean\n /** 그룹(타입)이 모델링 포커스 dim 처리됐는지. */\n isTypeDimmed: (type: string) => boolean\n /** 그룹(타입) 전원이 hidden(숨김)인지 — 그룹 hidden 토글 아이콘 판정. */\n isTypeHidden: (type: string) => boolean\n}\n\nfunction renderMember(entry: IndexEntry, terms: SearchTerm[], selected: Component[]): TemplateResult {\n const component = entry.component\n return html`\n <div class=\"component group-member\" ?selected=${selected.indexOf(component) > -1} .component=${component}>\n <span>\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>${' '}</pre>\n ${entry.label\n ? html`<span class=\"name\">${highlight(entry.label, terms)}</span>`\n : html`<span class=\"muted\">(no id)</span>`}\n </span>\n </div>\n `\n}\n\nfunction renderGroup(type: string, entries: IndexEntry[], ctx: GroupViewContext): TemplateResult {\n // 전역 검색으로 멤버를 좁히고, 매치 없는 그룹은 숨긴다.\n const members = ctx.terms.length ? entries.filter(e => matchesQuery(e.component, ctx.terms)) : entries\n if (ctx.terms.length && members.length === 0) return html``\n\n const expanded = ctx.isGroupExpanded(type)\n\n return html`\n <div class=\"group\">\n <div\n class=\"group-header\"\n data-group-type=${type}\n ?dimmed=${ctx.isTypeDimmed(type)}\n ?hidden-group=${ctx.isTypeHidden(type)}\n >\n <span class=${expanded ? 'extended' : 'collapsed'}> </span>\n <span class=\"group-type\">${highlight(type, ctx.terms)}</span>\n <span class=\"group-count\">${members.length}</span>\n <md-icon class=\"group-hidden\" title=\"Show/hide this group\"\n >${ctx.isTypeHidden(type) ? 'visibility_off' : 'visibility'}</md-icon\n >\n ${type === 'group'\n ? // 'group'(순수 컨테이너)은 dim 이 무의미 — 자식이 각자 다른 type 이라 캔버스에서 안 흐려짐.\n nothing\n : html`<md-icon class=\"group-dim\" title=\"Dim this group in canvas (modeling focus)\"\n >${ctx.isTypeDimmed(type) ? 'blur_on' : 'blur_off'}</md-icon\n >`}\n </div>\n ${expanded ? members.map(entry => renderMember(entry, ctx.terms, ctx.selected)) : nothing}\n </div>\n `\n}\n\n/** 타입 그룹 뷰 전체 렌더 — 타입명 오름차순. */\nexport function renderInspectorGroupView(index: InspectorIndex | undefined, ctx: GroupViewContext): TemplateResult {\n if (!index) return html``\n const types = [...index.byType.keys()].sort((a, b) => a.localeCompare(b))\n return html`${types.map(type => renderGroup(type, index.byType.get(type)!, ctx))}`\n}\n"]}
@@ -0,0 +1,7 @@
1
+ import { TemplateResult } from 'lit';
2
+ import { SearchTerm } from './inspector-model.js';
3
+ /**
4
+ * text 안에서 terms 의 값(field 무관, 시각 표시용)에 일치하는 부분을 하이라이트.
5
+ * 매치가 없거나 terms 가 비면 원본 문자열을 그대로 반환.
6
+ */
7
+ export declare function highlight(text: string | undefined, terms: SearchTerm[]): TemplateResult | string;
@@ -0,0 +1,25 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * 검색 매치 하이라이트 — 표시 텍스트에서 검색어 값과 일치하는 부분을 <mark> 로 감싼다.
5
+ */
6
+ import { html } from 'lit';
7
+ function escapeRegExp(s) {
8
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9
+ }
10
+ /**
11
+ * text 안에서 terms 의 값(field 무관, 시각 표시용)에 일치하는 부분을 하이라이트.
12
+ * 매치가 없거나 terms 가 비면 원본 문자열을 그대로 반환.
13
+ */
14
+ export function highlight(text, terms) {
15
+ if (!text)
16
+ return text || '';
17
+ const values = [...new Set(terms.map(t => t.value).filter(Boolean))];
18
+ if (!values.length)
19
+ return text;
20
+ const re = new RegExp(`(${values.map(escapeRegExp).join('|')})`, 'ig');
21
+ const parts = text.split(re).filter(p => p !== '');
22
+ const valueSet = new Set(values);
23
+ return html `${parts.map(part => (valueSet.has(part.toLowerCase()) ? html `<mark>${part}</mark>` : part))}`;
24
+ }
25
+ //# sourceMappingURL=inspector-highlight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspector-highlight.js","sourceRoot":"","sources":["../../../../src/property-panel/inspector/inspector-highlight.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,IAAI,EAAkB,MAAM,KAAK,CAAA;AAI1C,SAAS,YAAY,CAAC,CAAS;IAC7B,OAAO,CAAC,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAA;AACjD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAwB,EAAE,KAAmB;IACrE,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,IAAI,EAAE,CAAA;IAC5B,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;IACpE,IAAI,CAAC,MAAM,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAE/B,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;IACtE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAA;IAClD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAA;IAEhC,OAAO,IAAI,CAAA,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA,SAAS,IAAI,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,CAAA;AAC3G,CAAC","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * 검색 매치 하이라이트 — 표시 텍스트에서 검색어 값과 일치하는 부분을 <mark> 로 감싼다.\n */\nimport { html, TemplateResult } from 'lit'\n\nimport { SearchTerm } from './inspector-model.js'\n\nfunction escapeRegExp(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * text 안에서 terms 의 값(field 무관, 시각 표시용)에 일치하는 부분을 하이라이트.\n * 매치가 없거나 terms 가 비면 원본 문자열을 그대로 반환.\n */\nexport function highlight(text: string | undefined, terms: SearchTerm[]): TemplateResult | string {\n if (!text) return text || ''\n const values = [...new Set(terms.map(t => t.value).filter(Boolean))]\n if (!values.length) return text\n\n const re = new RegExp(`(${values.map(escapeRegExp).join('|')})`, 'ig')\n const parts = text.split(re).filter(p => p !== '')\n const valueSet = new Set(values)\n\n return html`${parts.map(part => (valueSet.has(part.toLowerCase()) ? html`<mark>${part}</mark>` : part))}`\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import { Component } from '@hatiolab/things-scene';
2
+ /** 인덱스 1건 — 한 컴포넌트의 인스펙터 표시/조작에 필요한 메타. */
3
+ export interface IndexEntry {
4
+ component: Component;
5
+ /** 등록 타입(@sceneComponent 이름) — 그룹 키. */
6
+ type: string;
7
+ id?: string;
8
+ tag?: string;
9
+ clazz?: string;
10
+ /** state.name (검색 대상). */
11
+ name?: string;
12
+ /** 표시용 라벨: #id @tag .(class) — 없으면 undefined. */
13
+ label?: string;
14
+ hidden: boolean;
15
+ locked: boolean;
16
+ /** root(=0) 기준 깊이. flat 에는 depth>0 만 담긴다. */
17
+ depth: number;
18
+ parent?: Component;
19
+ isContainer: boolean;
20
+ }
21
+ export interface InspectorIndex {
22
+ root: Component;
23
+ /** 루트 제외, depth-first 순서의 전체 컴포넌트. */
24
+ flat: IndexEntry[];
25
+ /** 타입(state.type) → 해당 타입 엔트리 목록 (= "컴포넌트 그룹"). */
26
+ byType: Map<string, IndexEntry[]>;
27
+ }
28
+ /** scene root 를 순회해 flat 목록 + 타입 그룹 맵을 만든다. root 자신은 제외. */
29
+ export declare function buildInspectorIndex(root: Component): InspectorIndex;
30
+ export type SearchField = 'type' | 'id' | 'tag' | 'name' | 'class';
31
+ /** 검색어 1개 — field 지정 시 그 필드만, 없으면 전 필드 통합 부분일치. */
32
+ export interface SearchTerm {
33
+ field?: SearchField;
34
+ value: string;
35
+ }
36
+ /** 검색 문자열을 공백 토큰화해 SearchTerm[] 로 파싱. `field:value` 는 필드 스코프. 소문자화. */
37
+ export declare function parseQuery(raw: string): SearchTerm[];
38
+ /**
39
+ * 파싱된 검색어(AND)로 컴포넌트 매치 판정.
40
+ * - field 지정 term → 해당 필드 부분일치
41
+ * - bare term → type/name/id/tag/class 통합 부분일치
42
+ * 빈 terms 는 항상 매치.
43
+ */
44
+ export declare function matchesQuery(component: Component, terms: SearchTerm[]): boolean;
@@ -0,0 +1,72 @@
1
+ function toEntry(component, depth, parent) {
2
+ const { type, id, tag, name, class: clazz } = component.state;
3
+ const label = (id ? `#${id}` : '') + (tag ? `@${tag}` : '') + (clazz ? `.(${clazz})` : '');
4
+ return {
5
+ component,
6
+ type,
7
+ id,
8
+ tag,
9
+ clazz,
10
+ name,
11
+ label: label || undefined,
12
+ hidden: !!component.get('hidden'),
13
+ locked: !!component.get('locked'),
14
+ depth,
15
+ parent,
16
+ isContainer: component.isContainer()
17
+ };
18
+ }
19
+ /** scene root 를 순회해 flat 목록 + 타입 그룹 맵을 만든다. root 자신은 제외. */
20
+ export function buildInspectorIndex(root) {
21
+ const flat = [];
22
+ const byType = new Map();
23
+ const visit = (component, depth, parent) => {
24
+ if (depth > 0) {
25
+ const entry = toEntry(component, depth, parent);
26
+ flat.push(entry);
27
+ const list = byType.get(entry.type);
28
+ if (list)
29
+ list.push(entry);
30
+ else
31
+ byType.set(entry.type, [entry]);
32
+ }
33
+ if (component.isContainer()) {
34
+ for (const child of component.components)
35
+ visit(child, depth + 1, component);
36
+ }
37
+ };
38
+ visit(root, 0);
39
+ return { root, flat, byType };
40
+ }
41
+ const SEARCH_FIELDS = ['type', 'id', 'tag', 'name', 'class'];
42
+ const FIELD_TOKEN_RE = /^(type|id|tag|name|class):(.*)$/;
43
+ /** 검색 문자열을 공백 토큰화해 SearchTerm[] 로 파싱. `field:value` 는 필드 스코프. 소문자화. */
44
+ export function parseQuery(raw) {
45
+ return (raw || '')
46
+ .toLowerCase()
47
+ .trim()
48
+ .split(/\s+/)
49
+ .filter(Boolean)
50
+ .map((token) => {
51
+ const m = token.match(FIELD_TOKEN_RE);
52
+ return m && m[2] ? { field: m[1], value: m[2] } : { value: token };
53
+ })
54
+ .filter(term => term.value);
55
+ }
56
+ function fieldValue(component, field) {
57
+ const v = component.state[field];
58
+ return v == null ? '' : String(v).toLowerCase();
59
+ }
60
+ /**
61
+ * 파싱된 검색어(AND)로 컴포넌트 매치 판정.
62
+ * - field 지정 term → 해당 필드 부분일치
63
+ * - bare term → type/name/id/tag/class 통합 부분일치
64
+ * 빈 terms 는 항상 매치.
65
+ */
66
+ export function matchesQuery(component, terms) {
67
+ if (!terms.length)
68
+ return true;
69
+ const combined = SEARCH_FIELDS.map(f => fieldValue(component, f)).join(' ');
70
+ return terms.every(term => term.field ? fieldValue(component, term.field).indexOf(term.value) > -1 : combined.indexOf(term.value) > -1);
71
+ }
72
+ //# sourceMappingURL=inspector-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inspector-model.js","sourceRoot":"","sources":["../../../../src/property-panel/inspector/inspector-model.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Scene Inspector 모델 인덱스.\n *\n * scene 컴포넌트 트리를 순회해, 뷰(트리 / 타입 그룹)와 focus 필터가 공유하는 데이터 구조로 만든다.\n * 순수 함수(DOM 비의존) — 테스트 가능하며, 향후 타입 그룹 뷰(P1)·포커스 dim 필터(P2.5)의 토대.\n *\n * 여기서 말하는 \"컴포넌트 그룹\"은 `state.type` 기준의 **논리 버킷**이다\n * (things-scene 의 실제 Group/Container 계층과 무관 — 계획서 §4.0 참조).\n */\nimport { Component, Container } from '@hatiolab/things-scene'\n\n/** 인덱스 1건 — 한 컴포넌트의 인스펙터 표시/조작에 필요한 메타. */\nexport interface IndexEntry {\n component: Component\n /** 등록 타입(@sceneComponent 이름) — 그룹 키. */\n type: string\n id?: string\n tag?: string\n clazz?: string\n /** state.name (검색 대상). */\n name?: string\n /** 표시용 라벨: #id @tag .(class) — 없으면 undefined. */\n label?: string\n hidden: boolean\n locked: boolean\n /** root(=0) 기준 깊이. flat 에는 depth>0 만 담긴다. */\n depth: number\n parent?: Component\n isContainer: boolean\n}\n\nexport interface InspectorIndex {\n root: Component\n /** 루트 제외, depth-first 순서의 전체 컴포넌트. */\n flat: IndexEntry[]\n /** 타입(state.type) → 해당 타입 엔트리 목록 (= \"컴포넌트 그룹\"). */\n byType: Map<string, IndexEntry[]>\n}\n\nfunction toEntry(component: Component, depth: number, parent?: Component): IndexEntry {\n const { type, id, tag, name, class: clazz } = component.state\n const label = (id ? `#${id}` : '') + (tag ? `@${tag}` : '') + (clazz ? `.(${clazz})` : '')\n return {\n component,\n type,\n id,\n tag,\n clazz,\n name,\n label: label || undefined,\n hidden: !!component.get('hidden'),\n locked: !!component.get('locked'),\n depth,\n parent,\n isContainer: component.isContainer()\n }\n}\n\n/** scene root 를 순회해 flat 목록 + 타입 그룹 맵을 만든다. root 자신은 제외. */\nexport function buildInspectorIndex(root: Component): InspectorIndex {\n const flat: IndexEntry[] = []\n const byType = new Map<string, IndexEntry[]>()\n\n const visit = (component: Component, depth: number, parent?: Component) => {\n if (depth > 0) {\n const entry = toEntry(component, depth, parent)\n flat.push(entry)\n const list = byType.get(entry.type)\n if (list) list.push(entry)\n else byType.set(entry.type, [entry])\n }\n if (component.isContainer()) {\n for (const child of (component as Container).components) visit(child, depth + 1, component)\n }\n }\n\n visit(root, 0)\n return { root, flat, byType }\n}\n\n// ── 검색 쿼리 (필드 토큰) ──\n\nexport type SearchField = 'type' | 'id' | 'tag' | 'name' | 'class'\nconst SEARCH_FIELDS: readonly SearchField[] = ['type', 'id', 'tag', 'name', 'class']\n\n/** 검색어 1개 — field 지정 시 그 필드만, 없으면 전 필드 통합 부분일치. */\nexport interface SearchTerm {\n field?: SearchField\n value: string\n}\n\nconst FIELD_TOKEN_RE = /^(type|id|tag|name|class):(.*)$/\n\n/** 검색 문자열을 공백 토큰화해 SearchTerm[] 로 파싱. `field:value` 는 필드 스코프. 소문자화. */\nexport function parseQuery(raw: string): SearchTerm[] {\n return (raw || '')\n .toLowerCase()\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .map((token): SearchTerm => {\n const m = token.match(FIELD_TOKEN_RE)\n return m && m[2] ? { field: m[1] as SearchField, value: m[2] } : { value: token }\n })\n .filter(term => term.value)\n}\n\nfunction fieldValue(component: Component, field: SearchField): string {\n const v = (component.state as Record<string, unknown>)[field]\n return v == null ? '' : String(v).toLowerCase()\n}\n\n/**\n * 파싱된 검색어(AND)로 컴포넌트 매치 판정.\n * - field 지정 term → 해당 필드 부분일치\n * - bare term → type/name/id/tag/class 통합 부분일치\n * 빈 terms 는 항상 매치.\n */\nexport function matchesQuery(component: Component, terms: SearchTerm[]): boolean {\n if (!terms.length) return true\n const combined = SEARCH_FIELDS.map(f => fieldValue(component, f)).join(' ')\n return terms.every(term =>\n term.field ? fieldValue(component, term.field).indexOf(term.value) > -1 : combined.indexOf(term.value) > -1\n )\n}\n"]}
@@ -0,0 +1,18 @@
1
+ import { Component } from '@hatiolab/things-scene';
2
+ import { TemplateResult } from 'lit';
3
+ import { SearchTerm } from './inspector-model.js';
4
+ /** 트리 렌더에 필요한 컨텍스트(호스트/컨트롤러가 주입). */
5
+ export interface TreeViewContext {
6
+ /** 현재 선택 컴포넌트 목록. */
7
+ selected: Component[];
8
+ /** 파싱된 검색어(AND). */
9
+ terms: SearchTerm[];
10
+ /** ROOT 노드에 표시할 매치 개수. */
11
+ count: number;
12
+ isExtended: (c: Component) => boolean;
13
+ nodeHandleClass: (c: Component) => string;
14
+ }
15
+ /** 검색 매치 개수(루트 포함, 전체 트리) — ROOT 노드 count 표시용. */
16
+ export declare function countMatches(component: Component, terms: SearchTerm[]): number;
17
+ /** scene.root 부터 트리를 렌더한다. */
18
+ export declare function renderInspectorTree(root: Component | undefined, ctx: TreeViewContext): TemplateResult;