@operato/property-panel 10.0.0-beta.60 → 10.0.0-beta.62
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 +32 -0
- package/dist/src/graphql/data-subscription.js +5 -1
- package/dist/src/graphql/data-subscription.js.map +1 -1
- package/dist/src/ox-property-panel.d.ts +10 -0
- package/dist/src/ox-property-panel.js +44 -6
- package/dist/src/ox-property-panel.js.map +1 -1
- package/dist/src/property-panel/effects/property-event-hover.d.ts +1 -1
- package/dist/src/property-panel/inspector/inspector-controller.d.ts +44 -0
- package/dist/src/property-panel/inspector/inspector-controller.js +189 -0
- package/dist/src/property-panel/inspector/inspector-controller.js.map +1 -0
- package/dist/src/property-panel/inspector/inspector-group-view.d.ts +13 -0
- package/dist/src/property-panel/inspector/inspector-group-view.js +48 -0
- package/dist/src/property-panel/inspector/inspector-group-view.js.map +1 -0
- package/dist/src/property-panel/inspector/inspector-highlight.d.ts +7 -0
- package/dist/src/property-panel/inspector/inspector-highlight.js +25 -0
- package/dist/src/property-panel/inspector/inspector-highlight.js.map +1 -0
- package/dist/src/property-panel/inspector/inspector-model.d.ts +44 -0
- package/dist/src/property-panel/inspector/inspector-model.js +72 -0
- package/dist/src/property-panel/inspector/inspector-model.js.map +1 -0
- package/dist/src/property-panel/inspector/inspector-tree-view.d.ts +18 -0
- package/dist/src/property-panel/inspector/inspector-tree-view.js +61 -0
- package/dist/src/property-panel/inspector/inspector-tree-view.js.map +1 -0
- package/dist/src/property-panel/inspector/inspector.d.ts +17 -12
- package/dist/src/property-panel/inspector/inspector.js +251 -177
- package/dist/src/property-panel/inspector/inspector.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,48 @@
|
|
|
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 class="group-header" data-group-type=${type} ?dimmed=${ctx.isTypeDimmed(type)}>
|
|
30
|
+
<span class=${expanded ? 'extended' : 'collapsed'}> </span>
|
|
31
|
+
<span class="group-type">${highlight(type, ctx.terms)}</span>
|
|
32
|
+
<span class="group-count">${members.length}</span>
|
|
33
|
+
<md-icon class="group-dim" title="Dim this group in canvas (modeling focus)"
|
|
34
|
+
>${ctx.isTypeDimmed(type) ? 'blur_on' : 'blur_off'}</md-icon
|
|
35
|
+
>
|
|
36
|
+
</div>
|
|
37
|
+
${expanded ? members.map(entry => renderMember(entry, ctx.terms, ctx.selected)) : nothing}
|
|
38
|
+
</div>
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
/** 타입 그룹 뷰 전체 렌더 — 타입명 오름차순. */
|
|
42
|
+
export function renderInspectorGroupView(index, ctx) {
|
|
43
|
+
if (!index)
|
|
44
|
+
return html ``;
|
|
45
|
+
const types = [...index.byType.keys()].sort((a, b) => a.localeCompare(b));
|
|
46
|
+
return html `${types.map(type => renderGroup(type, index.byType.get(type), ctx))}`;
|
|
47
|
+
}
|
|
48
|
+
//# 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":"AAaA,OAAO,EAAE,IAAI,EAAE,OAAO,EAAkB,MAAM,KAAK,CAAA;AAEnD,OAAO,EAA8B,YAAY,EAAc,MAAM,sBAAsB,CAAA;AAC3F,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AAWpD,SAAS,YAAY,CAAC,KAAiB,EAAE,KAAmB,EAAE,QAAqB;IACjF,MAAM,SAAS,GAAG,KAAK,CAAC,SAAS,CAAA;IACjC,OAAO,IAAI,CAAA;oDACuC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,eAAe,SAAS;;+BAE7E,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,YAAY;wCAChD,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC;aACpD,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW;;eAE5C,IAAI;UACT,KAAK,CAAC,KAAK;QACX,CAAC,CAAC,IAAI,CAAA,sBAAsB,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS;QAClE,CAAC,CAAC,IAAI,CAAA,oCAAoC;;;GAGjD,CAAA;AACH,CAAC;AAED,SAAS,WAAW,CAAC,IAAY,EAAE,OAAqB,EAAE,GAAqB;IAC7E,kCAAkC;IAClC,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAA;IACtG,IAAI,GAAG,CAAC,KAAK,CAAC,MAAM,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA,EAAE,CAAA;IAE3D,MAAM,QAAQ,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAA;IAE1C,OAAO,IAAI,CAAA;;kDAEqC,IAAI,YAAY,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC;sBAClE,QAAQ,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,WAAW;mCACtB,SAAS,CAAC,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC;oCACzB,OAAO,CAAC,MAAM;;aAErC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU;;;QAGpD,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO;;GAE5F,CAAA;AACH,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,wBAAwB,CAAC,KAAiC,EAAE,GAAqB;IAC/F,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA,EAAE,CAAA;IACzB,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAA;IACzE,OAAO,IAAI,CAAA,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAE,EAAE,GAAG,CAAC,CAAC,EAAE,CAAA;AACpF,CAAC","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}\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 class=\"group-header\" data-group-type=${type} ?dimmed=${ctx.isTypeDimmed(type)}>\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-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;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { html, nothing } from 'lit';
|
|
2
|
+
import { matchesQuery } from './inspector-model.js';
|
|
3
|
+
import { highlight } from './inspector-highlight.js';
|
|
4
|
+
/** 컴포넌트 자신 또는 자손 중 검색 매치가 있으면 표시 대상. */
|
|
5
|
+
function shouldBeShown(component, terms) {
|
|
6
|
+
if (matchesQuery(component, terms))
|
|
7
|
+
return true;
|
|
8
|
+
const children = component.isContainer() ? component.components : [];
|
|
9
|
+
return children.some(child => shouldBeShown(child, terms));
|
|
10
|
+
}
|
|
11
|
+
/** 검색 매치 개수(루트 포함, 전체 트리) — ROOT 노드 count 표시용. */
|
|
12
|
+
export function countMatches(component, terms) {
|
|
13
|
+
let n = matchesQuery(component, terms) ? 1 : 0;
|
|
14
|
+
const children = component.isContainer() ? component.components : [];
|
|
15
|
+
for (const child of children)
|
|
16
|
+
n += countMatches(child, terms);
|
|
17
|
+
return n;
|
|
18
|
+
}
|
|
19
|
+
function renderNode(component, depth, ctx) {
|
|
20
|
+
if (!component || !shouldBeShown(component, ctx.terms)) {
|
|
21
|
+
return html ``;
|
|
22
|
+
}
|
|
23
|
+
const children = component.isContainer() ? component.components : [];
|
|
24
|
+
const extended = ctx.isExtended(component) ? children.filter(child => shouldBeShown(child, ctx.terms)) : [];
|
|
25
|
+
const { type, id, tag, class: clazz } = component.state;
|
|
26
|
+
const label = (id ? `#${id}` : '') + (tag ? `@${tag}` : '') + (clazz ? `.(${clazz})` : '');
|
|
27
|
+
return html `
|
|
28
|
+
<div
|
|
29
|
+
class="component"
|
|
30
|
+
?selected=${ctx.selected.indexOf(component) > -1}
|
|
31
|
+
.component=${component}
|
|
32
|
+
?sortable=${component.isContainer()}
|
|
33
|
+
>
|
|
34
|
+
<span>
|
|
35
|
+
${depth > 0
|
|
36
|
+
? html `
|
|
37
|
+
<md-icon class="eye">${component.get('hidden') ? 'visibility_off' : 'visibility'}</md-icon>
|
|
38
|
+
<md-icon class="lock" ?locked=${!!component.get('locked')}
|
|
39
|
+
>${component.get('locked') ? 'lock' : 'lock_open'}</md-icon
|
|
40
|
+
>
|
|
41
|
+
<pre>${' '.repeat(depth)}</pre>
|
|
42
|
+
`
|
|
43
|
+
: html ` <pre>${' '.repeat(depth + 2)}</pre> `}
|
|
44
|
+
|
|
45
|
+
<span class=${ctx.nodeHandleClass(component)}> </span>
|
|
46
|
+
|
|
47
|
+
<span class="type">${depth == 0 ? html `ROOT(count: ${ctx.count})` : highlight(type, ctx.terms)}</span>
|
|
48
|
+
${label ? html ` <span class="name">${highlight(label, ctx.terms)}</span> ` : nothing}
|
|
49
|
+
</span>
|
|
50
|
+
|
|
51
|
+
${extended.map(child => renderNode(child, depth + 1, ctx))}
|
|
52
|
+
</div>
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
/** scene.root 부터 트리를 렌더한다. */
|
|
56
|
+
export function renderInspectorTree(root, ctx) {
|
|
57
|
+
if (!root)
|
|
58
|
+
return html ``;
|
|
59
|
+
return renderNode(root, 0, ctx);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=inspector-tree-view.js.map
|
|
@@ -0,0 +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,27 +1,32 @@
|
|
|
1
1
|
import '@operato/input/ox-input-search.js';
|
|
2
2
|
import { LitElement, PropertyValues, TemplateResult } from 'lit';
|
|
3
|
-
import {
|
|
3
|
+
import { Scene } from '@hatiolab/things-scene';
|
|
4
4
|
export declare class SceneInspector extends LitElement {
|
|
5
5
|
static styles: import("lit").CSSResult[];
|
|
6
6
|
scene?: Scene;
|
|
7
7
|
private searchText;
|
|
8
|
-
private
|
|
8
|
+
private viewMode;
|
|
9
|
+
private controller;
|
|
9
10
|
private show;
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
/** reveal(스크롤) 판정용 직전 선택. */
|
|
12
|
+
private _lastSelected;
|
|
12
13
|
disconnectedCallback(): void;
|
|
13
14
|
render(): TemplateResult<1>;
|
|
15
|
+
private _renderResult;
|
|
16
|
+
private _setViewMode;
|
|
17
|
+
private _collapseGroups;
|
|
18
|
+
private _expandGroups;
|
|
14
19
|
firstUpdated(): void;
|
|
20
|
+
/** 외부에서 강제 재렌더용(기존 공개 메쏘드 유지). */
|
|
15
21
|
refresh(): void;
|
|
16
22
|
updated(change: PropertyValues<this>): void;
|
|
23
|
+
/**
|
|
24
|
+
* 선택이 바뀌면(캔버스 등) 해당 항목을 리스트에 노출한다 — 그룹 뷰면 그룹을 펼친 뒤 스크롤.
|
|
25
|
+
* scrollIntoView({block:'nearest'}) 라 이미 보이는 경우(인스펙터 클릭 등)엔 사실상 no-op.
|
|
26
|
+
*/
|
|
27
|
+
private _maybeReveal;
|
|
28
|
+
/** 이벤트 타겟에서 상위로 올라가며 `.component` 를 가진 요소를 찾는다. */
|
|
29
|
+
private _resolveComponent;
|
|
17
30
|
_onclick(e: MouseEvent): void;
|
|
18
31
|
_ondblclick(e: MouseEvent): void;
|
|
19
|
-
get extendedMap(): any;
|
|
20
|
-
getNodeHandleClass(component: Component): "extended" | "collapsed" | "collapsespace";
|
|
21
|
-
isExtended(component: Component): boolean;
|
|
22
|
-
toggleExtended(component: Component): void;
|
|
23
|
-
toggleHidden(component: Component): void;
|
|
24
|
-
selectComponent(component: Component, append?: boolean): void;
|
|
25
|
-
shouldBeShown(component: Component, counting?: boolean): boolean;
|
|
26
|
-
renderComponent(component: Component, depth: number): TemplateResult;
|
|
27
32
|
}
|