@operato/slot-layout-editor 0.0.0-stage → 10.29.15

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ox-property-editor-slot-layout.js","sourceRoot":"","sources":["../../src/ox-property-editor-slot-layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAA;AAC1B,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAE,gBAAgB,EAAqB,MAAM,0BAA0B,CAAA;AAE9E,OAAO,4BAA4B,CAAA;AAS5B,IAAM,0BAA0B,GAAhC,MAAM,0BAA2B,SAAQ,gBAAgB;IAAzD;;QACG,aAAQ,GAA4B,EAAE,CAAA;QACtC,gBAAW,GAAY,MAAM,CAAC,MAAM,CAAC,CAAA;IA0C/C,CAAC;IAxCC,cAAc,CAAC,KAAU,EAAE,IAAkB;;QAC3C,uDAAuD;QACvD,IAAI,IAAI,CAAC,WAAW,KAAK,KAAK,EAAE,CAAC;YAC/B,IAAI,CAAC,WAAW,GAAG,KAAK,CAAA;YACxB,IAAI,CAAC,QAAQ,GAAG,MAAA,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,mCAAI,EAAE,CAAA;QAClD,CAAC;QACD,OAAO,IAAI,CAAA;;;iBAGE,KAAK;mBACH,IAAI,CAAC,QAAQ;mCACG,IAAI,CAAC,aAAa;;KAEhD,CAAA;IACH,CAAC;IAED,8EAA8E;IACtE,eAAe,CAAC,IAAkB;;QACxC,MAAM,IAAI,GAAG,MAAC,IAAI,CAAC,QAAkD,0CAAE,OAAO,CAAA;QAC9E,IAAI,OAAO,IAAI,KAAK,UAAU;YAAE,OAAO,SAAS,CAAA;QAChD,IAAI,QAA+B,CAAA;QACnC,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,iBAAiB,EAAE;YACjC,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAY,EAAE,EAAE,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE;SACvD,CAAC,CACH,CAAA;QACD,OAAO,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAC9C,CAAC;IAEO,aAAa,CAAC,CAAQ;QAC5B,CAAC,CAAC,eAAe,EAAE,CAAA;QACnB,6EAA6E;QAC7E,iDAAiD;QACjD,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,sBAAsB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;QACxI,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC;YAAE,OAAM;QACvC,IAAI,CAAC,KAAK,GAAG,SAAS,CAAA;QACtB,IAAI,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAC5E,CAAC;CACF,CAAA;AA5CY,0BAA0B;IADtC,aAAa,CAAC,gCAAgC,CAAC;GACnC,0BAA0B,CA4CtC","sourcesContent":["/**\n * @license Copyright © HatioLab Inc. All rights reserved.\n *\n * 속성 패널용 자리 레이아웃 편집기 (편집기 타입 'slot-layout').\n *\n * 기존 속성 편집기 계약을 따른다: 값은 this.value, 바뀌면 change 를 보내고 호스트(속성 패널)가 컴포넌트에 반영한다.\n * 선택 컴포넌트가 필요한 옵션(값의 출처, 기본값, 상태 문구)은 'i-need-selected' 로 선택 목록을 받아\n * 속성 정의의 property.options(selected) 가 만든다. 편집기는 컴포넌트를 직접 바꾸지 않는다.\n *\n * [기본값 사용]은 값을 비우는 편집과 구분해 'property-use-default'(detail.name, 취소 가능) 를 먼저 알린다.\n * 호스트가 그것을 처리하고 preventDefault() 하면 끝이고, 아니면 값을 비우는 change 하나를 보낸다.\n * 그 뒤 보일 기본값은 호스트가 property.options 로 다시 준다.\n *\n * 속성 정의 예:\n * { type: 'slot-layout', name: 'loadLayout', label: 'load-slots',\n * property: { options: selected => ({ effective, notes, presets, ... }) } }\n */\n\nimport { html } from 'lit'\nimport { customElement } from 'lit/decorators.js'\nimport { OxPropertyEditor, type PropertySpec } from '@operato/property-editor'\n\nimport './ox-slot-layout-editor.js'\nimport type { SlotLayoutEditorOptions } from './ox-slot-layout-editor.js'\n\nexport interface SlotLayoutPropertyOptions {\n /** 선택 컴포넌트로 편집기 옵션을 만든다. 값이 바뀔 때마다 다시 부른다 */\n options?: (selected: unknown[]) => SlotLayoutEditorOptions | undefined\n}\n\n@customElement('ox-property-editor-slot-layout')\nexport class OxPropertyEditorSlotLayout extends OxPropertyEditor {\n private _options: SlotLayoutEditorOptions = {}\n private _optionsFor: unknown = Symbol('none')\n\n editorTemplate(value: any, spec: PropertySpec) {\n // 값이 바뀌었을 때만 옵션을 다시 만든다 (렌더마다 새 옵션을 주면 편집 중인 값이 초기화된다)\n if (this._optionsFor !== value) {\n this._optionsFor = value\n this._options = this._resolveOptions(spec) ?? {}\n }\n return html`\n <ox-slot-layout-editor\n id=\"editor\"\n .value=${value}\n .options=${this._options}\n @slot-layout-use-default=${this._onUseDefault}\n ></ox-slot-layout-editor>\n `\n }\n\n /** 'i-need-selected' 로 선택 목록을 받아 property.options 를 부른다. 받을 호스트가 없으면 옵션 없음 */\n private _resolveOptions(spec: PropertySpec): SlotLayoutEditorOptions | undefined {\n const make = (spec.property as SlotLayoutPropertyOptions | undefined)?.options\n if (typeof make !== 'function') return undefined\n let selected: unknown[] | undefined\n this.dispatchEvent(\n new CustomEvent('i-need-selected', {\n bubbles: true,\n composed: true,\n detail: { callback: (s: unknown[]) => (selected = s) }\n })\n )\n return selected ? make(selected) : undefined\n }\n\n private _onUseDefault(e: Event) {\n e.stopPropagation()\n // 의도를 먼저 알린다. 호스트가 직접 처리하고 preventDefault() 하면 값 변경을 보내지 않는다 (두 번 저장하지 않도록).\n // 처리하지 않는 호스트(기존 속성 패널)에는 값을 비우는 change 하나로 알린다.\n const intent = new CustomEvent('property-use-default', { bubbles: true, composed: true, cancelable: true, detail: { name: this.name } })\n if (!this.dispatchEvent(intent)) return\n this.value = undefined\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ox-property-editor-slot-layout': OxPropertyEditorSlotLayout\n }\n}\n"]}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * @license Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * 자리 레이아웃 편집기. 홀더의 Tier·Slot 구성을 고치고, 검증하고, 미리 보고, [적용]으로 알린다.
5
+ * Tier 는 홀더가 주는 Anchor 하나에 서고, 그 Anchor 기준 위치·회전과 적재 방식(mount)을 갖는다.
6
+ *
7
+ * 컴포넌트 상태를 직접 바꾸지 않는다. 호스트에 두 가지만 알린다:
8
+ * change — 적용한 레이아웃 (this.value)
9
+ * slot-layout-use-default — 개별 설정을 지우고 기본값을 쓰겠다는 의도 (값을 비우는 편집과 다르다)
10
+ * 기본값을 쓰게 된 뒤 보일 값은 호스트가 options.effective 로 다시 준다.
11
+ */
12
+ import { LitElement } from 'lit';
13
+ import { type SlotLayout, type SlotSpaceOptions } from '@hatiolab/things-scene';
14
+ import { type PresetSize, type SlotLayoutPreset } from './draft.js';
15
+ import { type EditorIssue } from './issue-text.js';
16
+ export interface SlotLayoutEditorNote {
17
+ text: string;
18
+ level?: 'info' | 'error';
19
+ }
20
+ export interface SlotLayoutEditorOptions<L extends SlotLayout = SlotLayout> {
21
+ /** 개별 설정이 없을 때 보이는 값 (호스트가 고른 기본값). 편집 시작점이 된다 */
22
+ effective?: L;
23
+ /** [기본값 사용]을 보일지. 기본은 개별 설정(value)이 있을 때 */
24
+ canUseDefault?: boolean;
25
+ /** 호스트가 주는 프리셋 */
26
+ presets?: SlotLayoutPreset<L>[];
27
+ /** 프리셋 크기 초기값 */
28
+ presetSize?: Partial<PresetSize>;
29
+ /** 상태 문구 (값의 출처, 단위 상태 등) */
30
+ notes?: SlotLayoutEditorNote[];
31
+ /** 홀더가 주는 Anchor. 주면 Tier 는 이 중 하나를 고르고, 없는 Anchor 는 문제로 보인다 */
32
+ anchors?: {
33
+ id: string;
34
+ label?: string;
35
+ }[];
36
+ /** Anchor 가 주는 적재면 크기 (Anchor 기준 X·Z). 주면 자리가 면 밖으로 나가는지와 확인 불가를 알린다 */
37
+ anchorFaces?: SlotSpaceOptions['anchorFaces'];
38
+ /** 호스트 고유 검증 (경로는 레이아웃 기준) */
39
+ validate?: (layout: L) => EditorIssue[];
40
+ /** 문제 문구. undefined 를 돌려주면 기본 문구 */
41
+ issueText?: (issue: EditorIssue) => string | undefined;
42
+ /** 레이아웃에 덧붙인 호스트 키를 고치는 구역 (예: 실시간 주소 읽기). edit 로 새 레이아웃을 넘긴다 */
43
+ renderExtra?: (draft: L, edit: (next: L) => void) => unknown;
44
+ }
45
+ declare const OxSlotLayoutEditor_base: (new (...args: any[]) => LitElement) & typeof LitElement;
46
+ export declare class OxSlotLayoutEditor extends OxSlotLayoutEditor_base {
47
+ static styles: import("lit").CSSResult;
48
+ /** 개별 설정. 없으면 undefined */
49
+ value: SlotLayout | undefined;
50
+ options: SlotLayoutEditorOptions;
51
+ draft: SlotLayout | undefined;
52
+ tier: number;
53
+ dirty: boolean;
54
+ presetKey: string;
55
+ presetSize: Partial<PresetSize>;
56
+ presetParams: Record<string, number>;
57
+ willUpdate(changed: Map<string, unknown>): void;
58
+ /** 편집 중인 값을 버리고 개별 설정(없으면 호스트 기본값)에서 다시 시작한다 */
59
+ reset(): void;
60
+ get issues(): EditorIssue[];
61
+ /** 자리끼리 겹치거나 면 밖으로 나가는 설정 경고. 적용을 막지 않는다. 적재면 크기를 주지 않은 호스트에는 확인 불가를 보이지 않는다 */
62
+ get warnings(): EditorIssue[];
63
+ /** 적용하면 뜻이 바뀌는 주소 (지금 적용된 값 기준) */
64
+ get impact(): import("./draft.js").AddressImpact;
65
+ private _text;
66
+ private _edit;
67
+ private _apply;
68
+ private _useDefault;
69
+ render(): import("lit-html").TemplateResult<1>;
70
+ private _renderPreset;
71
+ /** 적용 전에 뜻이 바뀌는 주소를 보인다. 놓인 Carrier 는 주소로 다시 읽혀 다른 자리에 그려지거나 예외가 된다 */
72
+ private _renderImpact;
73
+ /** 프리셋으로 다시 시작한다. 지금 값에 덧붙은 호스트 키(Tier 밖의 키)는 가져온다 */
74
+ private _startFrom;
75
+ private _renderDraft;
76
+ /** 복사한 Tier 를 골라 둔다. 같은 자리에 겹쳐 생기므로 바로 이어서 배치를 고친다 */
77
+ private _copyTier;
78
+ private _renderTier;
79
+ /** Tier 가 선 Anchor, 적재 방식, Anchor 기준 위치·회전 */
80
+ private _renderPlacement;
81
+ /** 받는 타입: 모든 타입(accepts 생략) / 지정 타입만(빈 목록이면 아무것도 받지 않음) */
82
+ private _acceptsEditor;
83
+ private _renderSpec;
84
+ private _renderCustom;
85
+ /** 위에서 본 자리 배치. 오른쪽이 +X, 위쪽이 −Z */
86
+ private _renderPreview;
87
+ }
88
+ declare global {
89
+ interface HTMLElementTagNameMap {
90
+ 'ox-slot-layout-editor': OxSlotLayoutEditor;
91
+ }
92
+ }
93
+ export {};
@@ -0,0 +1,499 @@
1
+ /**
2
+ * @license Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * 자리 레이아웃 편집기. 홀더의 Tier·Slot 구성을 고치고, 검증하고, 미리 보고, [적용]으로 알린다.
5
+ * Tier 는 홀더가 주는 Anchor 하나에 서고, 그 Anchor 기준 위치·회전과 적재 방식(mount)을 갖는다.
6
+ *
7
+ * 컴포넌트 상태를 직접 바꾸지 않는다. 호스트에 두 가지만 알린다:
8
+ * change — 적용한 레이아웃 (this.value)
9
+ * slot-layout-use-default — 개별 설정을 지우고 기본값을 쓰겠다는 의도 (값을 비우는 편집과 다르다)
10
+ * 기본값을 쓰게 된 뒤 보일 값은 호스트가 options.effective 로 다시 준다.
11
+ */
12
+ import { __decorate } from "tslib";
13
+ import { css, html, nothing, LitElement, svg } from 'lit';
14
+ import { customElement, property, state } from 'lit/decorators.js';
15
+ import { i18next, localize } from '@operato/i18n';
16
+ import { persistenceIssues, slotSpaceWarnings, validateSlotLayout } from '@hatiolab/things-scene';
17
+ import { addCustomSlot, addressImpact, applyPreset, copyTier, previewTier, removeCustomSlot, removeTier, setTierPattern, updatePattern, updateSlotSpec, updateTier } from './draft.js';
18
+ import { issueText as defaultIssueText, spaceWarningText, t } from './issue-text.js';
19
+ const num = (e) => {
20
+ const n = e.target.valueAsNumber;
21
+ return Number.isFinite(n) ? n : NaN;
22
+ };
23
+ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitElement) {
24
+ constructor() {
25
+ super(...arguments);
26
+ this.options = {};
27
+ this.tier = 1;
28
+ this.dirty = false;
29
+ this.presetKey = '';
30
+ this.presetSize = {};
31
+ this.presetParams = {};
32
+ }
33
+ willUpdate(changed) {
34
+ var _a, _b, _c, _d, _e, _f, _g;
35
+ // 호스트가 렌더마다 새 options 를 넘겨도 편집 중인 값을 잃지 않도록, 값이나 호스트 기본값이 바뀔 때만 다시 시작한다
36
+ const before = changed.get('options');
37
+ if (changed.has('value') || (changed.has('options') && (before === null || before === void 0 ? void 0 : before.effective) !== ((_a = this.options) === null || _a === void 0 ? void 0 : _a.effective)))
38
+ this.reset();
39
+ else if (changed.has('options') && !((_c = (_b = this.options) === null || _b === void 0 ? void 0 : _b.presets) === null || _c === void 0 ? void 0 : _c.some(p => p.key === this.presetKey)))
40
+ this.presetKey = (_g = (_f = (_e = (_d = this.options) === null || _d === void 0 ? void 0 : _d.presets) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.key) !== null && _g !== void 0 ? _g : '';
41
+ }
42
+ /** 편집 중인 값을 버리고 개별 설정(없으면 호스트 기본값)에서 다시 시작한다 */
43
+ reset() {
44
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
45
+ const start = (_a = this.value) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.effective;
46
+ this.draft = start ? structuredClone(start) : undefined;
47
+ this.tier = (_f = (_e = (_d = (_c = this.draft) === null || _c === void 0 ? void 0 : _c.tiers) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e.tierNo) !== null && _f !== void 0 ? _f : 1;
48
+ this.dirty = false;
49
+ this.presetSize = { ...((_h = (_g = this.options) === null || _g === void 0 ? void 0 : _g.presetSize) !== null && _h !== void 0 ? _h : {}) };
50
+ if (!((_k = (_j = this.options) === null || _j === void 0 ? void 0 : _j.presets) === null || _k === void 0 ? void 0 : _k.some(p => p.key === this.presetKey)))
51
+ this.presetKey = (_p = (_o = (_m = (_l = this.options) === null || _l === void 0 ? void 0 : _l.presets) === null || _m === void 0 ? void 0 : _m[0]) === null || _o === void 0 ? void 0 : _o.key) !== null && _p !== void 0 ? _p : '';
52
+ }
53
+ get issues() {
54
+ var _a, _b, _c, _d, _e;
55
+ if (!this.draft)
56
+ return [];
57
+ const anchors = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.anchors) === null || _b === void 0 ? void 0 : _b.map(a => a.id);
58
+ const invalid = [...validateSlotLayout(this.draft, { anchors }), ...((_e = (_d = (_c = this.options) === null || _c === void 0 ? void 0 : _c.validate) === null || _d === void 0 ? void 0 : _d.call(_c, this.draft)) !== null && _e !== void 0 ? _e : [])];
59
+ // 저장 불가 검사는 검증을 통과한 값에만 보인다 (같은 문제를 두 번 적지 않는다)
60
+ return invalid.length ? invalid : persistenceIssues(this.draft);
61
+ }
62
+ /** 자리끼리 겹치거나 면 밖으로 나가는 설정 경고. 적용을 막지 않는다. 적재면 크기를 주지 않은 호스트에는 확인 불가를 보이지 않는다 */
63
+ get warnings() {
64
+ var _a;
65
+ if (!this.draft || this.issues.length)
66
+ return [];
67
+ const faces = (_a = this.options) === null || _a === void 0 ? void 0 : _a.anchorFaces;
68
+ return slotSpaceWarnings(this.draft, { anchorFaces: faces }).filter(w => faces || w.code !== 'ANCHOR_SPACE_UNKNOWN');
69
+ }
70
+ /** 적용하면 뜻이 바뀌는 주소 (지금 적용된 값 기준) */
71
+ get impact() {
72
+ var _a, _b;
73
+ return this.dirty && !this.issues.length ? addressImpact((_a = this.value) !== null && _a !== void 0 ? _a : (_b = this.options) === null || _b === void 0 ? void 0 : _b.effective, this.draft) : { moved: [], gone: [] };
74
+ }
75
+ _text(issue) {
76
+ var _a, _b, _c;
77
+ return (_c = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.issueText) === null || _b === void 0 ? void 0 : _b.call(_a, issue)) !== null && _c !== void 0 ? _c : defaultIssueText(issue);
78
+ }
79
+ _edit(next) {
80
+ var _a, _b;
81
+ this.draft = next;
82
+ this.dirty = true;
83
+ if (!next.tiers.some(x => x.tierNo === this.tier))
84
+ this.tier = (_b = (_a = next.tiers[next.tiers.length - 1]) === null || _a === void 0 ? void 0 : _a.tierNo) !== null && _b !== void 0 ? _b : 1;
85
+ }
86
+ _apply() {
87
+ this.value = structuredClone(this.draft);
88
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
89
+ }
90
+ _useDefault() {
91
+ this.dispatchEvent(new CustomEvent('slot-layout-use-default', { bubbles: true, composed: true }));
92
+ }
93
+ render() {
94
+ var _a, _b, _c, _d;
95
+ const issues = this.issues;
96
+ const canUseDefault = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.canUseDefault) !== null && _b !== void 0 ? _b : this.value !== undefined;
97
+ return html `
98
+ ${((_d = (_c = this.options) === null || _c === void 0 ? void 0 : _c.notes) === null || _d === void 0 ? void 0 : _d.length)
99
+ ? html `<div class="notes">
100
+ ${this.options.notes.map(n => html `<div class=${n.level === 'error' ? 'error' : 'note'}>${n.text}</div>`)}
101
+ </div>`
102
+ : nothing}
103
+ ${this._renderPreset()} ${this.draft ? this._renderDraft(issues) : nothing} ${this._renderImpact()}
104
+ <div class="actions">
105
+ ${canUseDefault ? html `<button data-action="use-default" @click=${this._useDefault}>${t('use-default', 'Use default')}</button>` : nothing}
106
+ <button data-action="discard" ?disabled=${!this.dirty} @click=${() => this.reset()}>${t('discard', 'Discard')}</button>
107
+ <button data-action="apply" ?disabled=${!this.dirty || !this.draft || issues.length > 0} @click=${this._apply}>${t('apply', 'Apply')}</button>
108
+ </div>
109
+ `;
110
+ }
111
+ _renderPreset() {
112
+ var _a, _b, _c, _d;
113
+ const presets = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.presets) !== null && _b !== void 0 ? _b : [];
114
+ if (!presets.length)
115
+ return nothing;
116
+ const preset = (_c = presets.find(p => p.key === this.presetKey)) !== null && _c !== void 0 ? _c : presets[0];
117
+ const built = applyPreset(preset, this.presetSize, this.presetParams);
118
+ const numberRow = (label, value, onChange, key) => html `
119
+ <div class="row">
120
+ <label>${label}</label>
121
+ <input type="number" data-preset=${key} .value=${value === undefined || Number.isNaN(value) ? '' : String(value)} @change=${(e) => onChange(num(e))} />
122
+ </div>
123
+ `;
124
+ return html `
125
+ <h4>${t('preset', 'Preset')}</h4>
126
+ <div class="row">
127
+ <label>${t('preset-kind', 'Kind')}</label>
128
+ <select data-preset="kind" @change=${(e) => (this.presetKey = e.target.value)}>
129
+ ${presets.map(p => html `<option value=${p.key} ?selected=${p.key === preset.key}>${p.label}</option>`)}
130
+ </select>
131
+ </div>
132
+ ${numberRow(t('width', 'Width (X)'), this.presetSize.width, n => (this.presetSize = { ...this.presetSize, width: n }), 'width')}
133
+ ${numberRow(t('depth', 'Depth (Z)'), this.presetSize.depth, n => (this.presetSize = { ...this.presetSize, depth: n }), 'depth')}
134
+ ${numberRow(t('slot-height', 'Slot height'), this.presetSize.slotHeight, n => (this.presetSize = { ...this.presetSize, slotHeight: n }), 'slotHeight')}
135
+ ${((_d = preset.params) !== null && _d !== void 0 ? _d : []).map(p => numberRow(p.label, this.presetParams[p.key], n => (this.presetParams = { ...this.presetParams, [p.key]: n }), p.key))}
136
+ <div class="note">${t('preset-note', 'Pitch and slot size are split evenly as starting values. Confirm them against the real equipment.')}</div>
137
+ <div class="actions">
138
+ <button data-action="start-preset" ?disabled=${!built.layout} @click=${() => built.layout && this._startFrom(built.layout)}>
139
+ ${t('start-from-preset', 'Start from preset')}
140
+ </button>
141
+ </div>
142
+ `;
143
+ }
144
+ /** 적용 전에 뜻이 바뀌는 주소를 보인다. 놓인 Carrier 는 주소로 다시 읽혀 다른 자리에 그려지거나 예외가 된다 */
145
+ _renderImpact() {
146
+ const { moved, gone } = this.impact;
147
+ if (!moved.length && !gone.length)
148
+ return nothing;
149
+ const list = (ids) => (ids.length > 12 ? `${ids.slice(0, 12).join(', ')} … (${ids.length})` : ids.join(', '));
150
+ return html `<div class="notes" data-impact>
151
+ ${moved.length ? html `<div class="note">${t('impact-moved', 'Moves: {ids}', { ids: list(moved) })}</div>` : nothing}
152
+ ${gone.length ? html `<div class="error">${t('impact-gone', 'Removed: {ids}', { ids: list(gone) })}</div>` : nothing}
153
+ </div>`;
154
+ }
155
+ /** 프리셋으로 다시 시작한다. 지금 값에 덧붙은 호스트 키(Tier 밖의 키)는 가져온다 */
156
+ _startFrom(layout) {
157
+ var _a;
158
+ const { tiers: _t, ...extra } = ((_a = this.draft) !== null && _a !== void 0 ? _a : {});
159
+ this._edit({ ...extra, ...structuredClone(layout) });
160
+ }
161
+ _renderDraft(issues) {
162
+ var _a, _b, _c, _d, _e, _f;
163
+ const draft = this.draft;
164
+ const tier = (_b = (_a = draft.tiers) === null || _a === void 0 ? void 0 : _a.find(x => x.tierNo === this.tier)) !== null && _b !== void 0 ? _b : (_c = draft.tiers) === null || _c === void 0 ? void 0 : _c[0];
165
+ const warnings = this.warnings;
166
+ const issueList = issues.length
167
+ ? html `<div class="error" data-issues>${issues.map(i => html `<div>· ${this._text(i)}</div>`)}</div>`
168
+ : warnings.length
169
+ ? html `<div class="note" data-warnings>${warnings.map(w => html `<div>· ${spaceWarningText(w)}</div>`)}</div>`
170
+ : nothing;
171
+ // 층이 없는 값(잘못된 저장값)은 편집할 층이 없으므로 문제만 보인다. 프리셋으로 다시 시작한다
172
+ if (!tier)
173
+ return issueList;
174
+ return html `
175
+ <h4>${t('tiers', 'Tiers')}</h4>
176
+ <div class="chips">
177
+ ${draft.tiers.map(x => html `<button data-tier=${x.tierNo} ?active=${x.tierNo === tier.tierNo} @click=${() => (this.tier = x.tierNo)}>${t('tier-n', 'Tier {n}', { n: x.tierNo })}</button>`)}
178
+ </div>
179
+ <div class="actions">
180
+ <button data-action="copy-tier" @click=${() => this._copyTier(draft, tier.tierNo)}>${t('copy-tier', 'Copy tier')}</button>
181
+ <button data-action="remove-tier" ?disabled=${draft.tiers.length <= 1} @click=${() => this._edit(removeTier(draft, tier.tierNo))}>${t('remove-tier', 'Remove tier')}</button>
182
+ </div>
183
+ ${this._renderTier(draft, tier.tierNo)} ${(_f = (_e = (_d = this.options) === null || _d === void 0 ? void 0 : _d.renderExtra) === null || _e === void 0 ? void 0 : _e.call(_d, draft, next => this._edit(next))) !== null && _f !== void 0 ? _f : nothing}
184
+ <h4>${t('preview', 'Preview (top view, tier {n}, in the tier)', { n: tier.tierNo })}</h4>
185
+ ${this._renderPreview(draft, tier.tierNo)} ${issueList}
186
+ `;
187
+ }
188
+ /** 복사한 Tier 를 골라 둔다. 같은 자리에 겹쳐 생기므로 바로 이어서 배치를 고친다 */
189
+ _copyTier(draft, tierNo) {
190
+ const next = copyTier(draft, tierNo);
191
+ this._edit(next);
192
+ this.tier = next.tiers[next.tiers.length - 1].tierNo;
193
+ }
194
+ _renderTier(draft, tierNo) {
195
+ var _a, _b, _c, _d, _e, _f, _g, _h;
196
+ const tier = draft.tiers.find(x => x.tierNo === tierNo);
197
+ const l = tier.layout;
198
+ const patternInput = (k, label, value, parse = num) => html `
199
+ <div class="row">
200
+ <label>${label}</label>
201
+ <input type="number" data-field=${k} .value=${value === undefined || Number.isNaN(value) ? '' : String(value)} @change=${(e) => this._edit(updatePattern(draft, tierNo, { [k]: parse(e) }))} />
202
+ </div>
203
+ `;
204
+ const select = (field, label, value, options, onChange) => html `
205
+ <div class="row">
206
+ <label>${label}</label>
207
+ <select data-field=${field} @change=${(e) => onChange(e.target.value)}>
208
+ ${options.map(o => html `<option value=${o} ?selected=${o === value}>${o}</option>`)}
209
+ </select>
210
+ </div>
211
+ `;
212
+ return html `
213
+ ${this._renderPlacement(draft, tierNo)}
214
+ ${select('pattern', t('pattern', 'Pattern'), l.pattern, ['SINGLE', 'ROW', 'GRID', 'CUSTOM'], v => this._edit(setTierPattern(draft, tierNo, v)))}
215
+ ${l.pattern === 'ROW'
216
+ ? html `
217
+ ${patternInput('count', t('count', 'Slots'), l.count)} ${patternInput('pitch', t('pitch', 'Pitch'), l.pitch)}
218
+ ${select('axis', t('axis', 'Axis'), l.axis, ['X', 'Z'], v => this._edit(updatePattern(draft, tierNo, { axis: v })))}
219
+ ${select('origin', t('origin', 'Slot 1 at'), (_a = l.origin) !== null && _a !== void 0 ? _a : 'MIN', ['MIN', 'MAX'], v => this._edit(updatePattern(draft, tierNo, { origin: v })))}
220
+ `
221
+ : nothing}
222
+ ${l.pattern === 'GRID'
223
+ ? html `
224
+ ${patternInput('rows', t('rows', 'Rows (Z)'), l.rows)} ${patternInput('cols', t('cols', 'Columns (X)'), l.cols)}
225
+ ${patternInput('pitch', t('pitch-x', 'Pitch X'), (_b = l.pitch) === null || _b === void 0 ? void 0 : _b.x, e => ({ ...l.pitch, x: num(e) }))}
226
+ ${patternInput('pitch', t('pitch-z', 'Pitch Z'), (_c = l.pitch) === null || _c === void 0 ? void 0 : _c.z, e => ({ ...l.pitch, z: num(e) }))}
227
+ ${select('order', t('order', 'Numbering'), (_d = l.order) !== null && _d !== void 0 ? _d : 'ROW_MAJOR', ['ROW_MAJOR', 'COL_MAJOR', 'SERPENTINE_ROW', 'SERPENTINE_COL'], v => this._edit(updatePattern(draft, tierNo, { order: v })))}
228
+ ${select('origin-x', t('origin-x', 'Slot 1 at X'), (_f = (_e = l.origin) === null || _e === void 0 ? void 0 : _e.x) !== null && _f !== void 0 ? _f : 'MIN', ['MIN', 'MAX'], v => this._edit(updatePattern(draft, tierNo, { origin: { z: 'MIN', ...l.origin, x: v } })))}
229
+ ${select('origin-z', t('origin-z', 'Slot 1 at Z'), (_h = (_g = l.origin) === null || _g === void 0 ? void 0 : _g.z) !== null && _h !== void 0 ? _h : 'MIN', ['MIN', 'MAX'], v => this._edit(updatePattern(draft, tierNo, { origin: { x: 'MIN', ...l.origin, z: v } })))}
230
+ `
231
+ : nothing}
232
+ ${l.pattern === 'CUSTOM' ? this._renderCustom(draft, tierNo) : this._renderSpec(draft, tierNo, l.slot)}
233
+ `;
234
+ }
235
+ /** Tier 가 선 Anchor, 적재 방식, Anchor 기준 위치·회전 */
236
+ _renderPlacement(draft, tierNo) {
237
+ var _a, _b, _c, _d, _e, _f;
238
+ const tier = draft.tiers.find(x => x.tierNo === tierNo);
239
+ const anchors = (_a = this.options) === null || _a === void 0 ? void 0 : _a.anchors;
240
+ const edit = (patch) => this._edit(updateTier(draft, tierNo, patch));
241
+ const position = (_b = tier.position) !== null && _b !== void 0 ? _b : { x: 0, y: 0, z: 0 };
242
+ const axis = (k, label) => {
243
+ var _a;
244
+ return html `
245
+ <div class="row">
246
+ <label>${label}</label>
247
+ <input type="number" data-field=${`position-${k}`} .value=${String((_a = position[k]) !== null && _a !== void 0 ? _a : '')} @change=${(e) => edit({ position: { ...position, [k]: num(e) } })} />
248
+ </div>
249
+ `;
250
+ };
251
+ return html `
252
+ <div class="row">
253
+ <label>${t('anchor', 'Anchor')}</label>
254
+ ${(anchors === null || anchors === void 0 ? void 0 : anchors.length)
255
+ ? html `<select data-field="anchor" @change=${(e) => edit({ anchorId: e.target.value })}>
256
+ ${anchors.some(a => a.id === tier.anchorId) ? nothing : html `<option value=${(_c = tier.anchorId) !== null && _c !== void 0 ? _c : ''} selected>${(_d = tier.anchorId) !== null && _d !== void 0 ? _d : ''}</option>`}
257
+ ${anchors.map(a => { var _a; return html `<option value=${a.id} ?selected=${a.id === tier.anchorId}>${(_a = a.label) !== null && _a !== void 0 ? _a : a.id}</option>`; })}
258
+ </select>`
259
+ : html `<input data-field="anchor" .value=${(_e = tier.anchorId) !== null && _e !== void 0 ? _e : ''} @change=${(e) => edit({ anchorId: e.target.value.trim() })} />`}
260
+ </div>
261
+ <div class="row">
262
+ <label>${t('mount', 'Mount')}</label>
263
+ <select data-field="mount" @change=${(e) => edit({ mount: e.target.value })}>
264
+ ${['ON', 'IN', 'HANG'].map(m => html `<option value=${m} ?selected=${m === tier.mount}>${m}</option>`)}
265
+ </select>
266
+ </div>
267
+ ${axis('x', t('position-x', 'Position X'))} ${axis('y', t('position-y', 'Position Y (height)'))} ${axis('z', t('position-z', 'Position Z'))}
268
+ <div class="row">
269
+ <label>${t('tier-rotation', 'Tier rotation (deg)')}</label>
270
+ <input type="number" data-field="tier-rotation" .value=${String((_f = tier.rotation) !== null && _f !== void 0 ? _f : '')} @change=${(e) => edit({ rotation: num(e) })} />
271
+ </div>
272
+ `;
273
+ }
274
+ /** 받는 타입: 모든 타입(accepts 생략) / 지정 타입만(빈 목록이면 아무것도 받지 않음) */
275
+ _acceptsEditor(spec, onChange) {
276
+ var _a;
277
+ const restricted = Array.isArray(spec.accepts);
278
+ return html `
279
+ <div class="row">
280
+ <label>${t('accepts', 'Accepts')}</label>
281
+ <select data-field="accepts-mode" @change=${(e) => { var _a; return onChange(e.target.value === 'ALL' ? undefined : (_a = spec.accepts) !== null && _a !== void 0 ? _a : []); }}>
282
+ <option value="ALL" ?selected=${!restricted}>${t('accepts-all', 'Any type')}</option>
283
+ <option value="LIST" ?selected=${restricted}>${t('accepts-list', 'Listed types only')}</option>
284
+ </select>
285
+ </div>
286
+ ${restricted
287
+ ? html `<div class="row">
288
+ <label>${t('accepts-types', 'Types (comma)')}</label>
289
+ <input
290
+ data-field="accepts"
291
+ .value=${((_a = spec.accepts) !== null && _a !== void 0 ? _a : []).join(', ')}
292
+ placeholder=${t('accepts-none', 'Empty = accepts nothing')}
293
+ @change=${(e) => onChange(e.target.value
294
+ .split(',')
295
+ .map(s => s.trim())
296
+ .filter(Boolean))}
297
+ />
298
+ </div>`
299
+ : nothing}
300
+ `;
301
+ }
302
+ _renderSpec(draft, tierNo, spec) {
303
+ var _a, _b;
304
+ const edit = (patch) => this._edit(updateSlotSpec(draft, tierNo, patch));
305
+ const allowed = (k, label) => {
306
+ var _a, _b;
307
+ return html `
308
+ <div class="row">
309
+ <label>${label}</label>
310
+ <input type="number" data-field=${`allowed-${k}`} .value=${String((_b = (_a = spec.allowed) === null || _a === void 0 ? void 0 : _a[k]) !== null && _b !== void 0 ? _b : '')} @change=${(e) => edit({ allowed: { ...spec.allowed, [k]: num(e) } })} />
311
+ </div>
312
+ `;
313
+ };
314
+ return html `
315
+ <h4>${t('slot-spec', 'Slot (same for every slot in this tier)')}</h4>
316
+ ${allowed('width', t('allowed-width', 'Allowed width'))} ${allowed('depth', t('allowed-depth', 'Allowed depth'))}
317
+ ${allowed('height', t('allowed-height', 'Allowed height'))}
318
+ <div class="row">
319
+ <label>${t('stack-limit', 'Stack limit')}</label>
320
+ <input type="number" data-field="stack-limit" min="1" step="1" .value=${String((_a = spec.stackLimit) !== null && _a !== void 0 ? _a : 1)} @change=${(e) => edit({ stackLimit: num(e) })} />
321
+ </div>
322
+ <div class="row">
323
+ <label>${t('rotation', 'Rotation (deg)')}</label>
324
+ <input type="number" data-field="rotation" .value=${String((_b = spec.rotation) !== null && _b !== void 0 ? _b : 0)} @change=${(e) => edit({ rotation: num(e) || undefined })} />
325
+ </div>
326
+ ${this._acceptsEditor(spec, accepts => edit({ accepts }))}
327
+ `;
328
+ }
329
+ _renderCustom(draft, tierNo) {
330
+ const tier = draft.tiers.find(x => x.tierNo === tierNo);
331
+ const slots = [...tier.layout.slots].sort((a, b) => a.slotNo - b.slotNo);
332
+ const cell = (slotNo, value, patch) => html `<td><input type="number" .value=${String(value !== null && value !== void 0 ? value : '')} @change=${(e) => this._edit(updateSlotSpec(draft, tierNo, patch(e), slotNo))} /></td>`;
333
+ return html `
334
+ <h4>${t('custom-slots', 'Slots')}</h4>
335
+ <table>
336
+ <tr>
337
+ <th>#</th>
338
+ <th>X</th>
339
+ <th>Z</th>
340
+ <th>${t('w', 'W')}</th>
341
+ <th>${t('d', 'D')}</th>
342
+ <th>${t('h', 'H')}</th>
343
+ <th>${t('stack', 'Stack')}</th>
344
+ <th>${t('rot', 'Rot')}</th>
345
+ <th></th>
346
+ </tr>
347
+ ${slots.map(s => {
348
+ var _a, _b, _c, _d, _e, _f, _g;
349
+ return html `
350
+ <tr>
351
+ <td>${s.slotNo}</td>
352
+ ${cell(s.slotNo, (_a = s.offset) === null || _a === void 0 ? void 0 : _a.x, e => ({ offset: { ...s.offset, x: num(e) } }))}
353
+ ${cell(s.slotNo, (_b = s.offset) === null || _b === void 0 ? void 0 : _b.z, e => ({ offset: { ...s.offset, z: num(e) } }))}
354
+ ${cell(s.slotNo, (_c = s.allowed) === null || _c === void 0 ? void 0 : _c.width, e => ({ allowed: { ...s.allowed, width: num(e) } }))}
355
+ ${cell(s.slotNo, (_d = s.allowed) === null || _d === void 0 ? void 0 : _d.depth, e => ({ allowed: { ...s.allowed, depth: num(e) } }))}
356
+ ${cell(s.slotNo, (_e = s.allowed) === null || _e === void 0 ? void 0 : _e.height, e => ({ allowed: { ...s.allowed, height: num(e) } }))}
357
+ ${cell(s.slotNo, (_f = s.stackLimit) !== null && _f !== void 0 ? _f : 1, e => ({ stackLimit: num(e) }))} ${cell(s.slotNo, (_g = s.rotation) !== null && _g !== void 0 ? _g : 0, e => ({ rotation: num(e) || undefined }))}
358
+ <td><button ?disabled=${slots.length <= 1} @click=${() => this._edit(removeCustomSlot(draft, tierNo, s.slotNo))}>−</button></td>
359
+ </tr>
360
+ `;
361
+ })}
362
+ </table>
363
+ <div class="actions"><button data-action="add-slot" @click=${() => this._edit(addCustomSlot(draft, tierNo))}>${t('add-slot', 'Add slot')}</button></div>
364
+ `;
365
+ }
366
+ /** 위에서 본 자리 배치. 오른쪽이 +X, 위쪽이 −Z */
367
+ _renderPreview(draft, tierNo) {
368
+ const slots = previewTier(draft, tierNo);
369
+ if (!slots.length)
370
+ return html `<div class="note" data-preview="none">${t('preview-none', 'No slots to show with the current values.')}</div>`;
371
+ const minX = Math.min(...slots.map(s => s.x - s.width / 2));
372
+ const maxX = Math.max(...slots.map(s => s.x + s.width / 2));
373
+ const minZ = Math.min(...slots.map(s => s.z - s.depth / 2));
374
+ const maxZ = Math.max(...slots.map(s => s.z + s.depth / 2));
375
+ const w = Math.max(maxX - minX, 1);
376
+ const h = Math.max(maxZ - minZ, 1);
377
+ const pad = Math.max(w, h) * 0.08;
378
+ const font = Math.max(w, h) * 0.07;
379
+ return html `
380
+ <svg data-preview viewBox="${minX - pad} ${minZ - pad} ${w + pad * 2} ${h + pad * 2}" preserveAspectRatio="xMidYMid meet">
381
+ ${slots.map(s => svg `
382
+ <rect data-slot=${s.slotNo} x=${s.x - s.width / 2} y=${s.z - s.depth / 2} width=${s.width} height=${s.depth}
383
+ fill="rgba(25,118,210,0.08)" stroke="rgba(25,118,210,0.8)" stroke-width=${Math.max(w, h) * 0.006} />
384
+ <text x=${s.x} y=${s.z} font-size=${font} text-anchor="middle" dominant-baseline="central" fill="#0d47a1">${s.slotNo}</text>
385
+ `)}
386
+ </svg>
387
+ <div class="note">${t('preview-axes', 'Right = +X, up = −Z')}</div>
388
+ `;
389
+ }
390
+ };
391
+ OxSlotLayoutEditor.styles = css `
392
+ :host {
393
+ display: flex;
394
+ flex-direction: column;
395
+ gap: 6px;
396
+ font-size: 12px;
397
+ }
398
+ .row {
399
+ display: grid;
400
+ grid-template-columns: 40% 1fr;
401
+ align-items: center;
402
+ gap: 4px;
403
+ }
404
+ .notes {
405
+ padding: 4px 6px;
406
+ border-radius: 4px;
407
+ background: rgba(0, 0, 0, 0.04);
408
+ }
409
+ .error {
410
+ color: #c62828;
411
+ }
412
+ .note {
413
+ color: rgba(0, 0, 0, 0.55);
414
+ }
415
+ .chips {
416
+ display: flex;
417
+ flex-wrap: wrap;
418
+ gap: 4px;
419
+ }
420
+ .chips button[active] {
421
+ border-color: var(--md-sys-color-primary, #1976d2);
422
+ color: var(--md-sys-color-primary, #1976d2);
423
+ font-weight: bold;
424
+ }
425
+ button {
426
+ font-size: 12px;
427
+ padding: 2px 8px;
428
+ border: 1px solid rgba(0, 0, 0, 0.2);
429
+ border-radius: 4px;
430
+ background: #fff;
431
+ cursor: pointer;
432
+ }
433
+ button[disabled] {
434
+ opacity: 0.4;
435
+ cursor: default;
436
+ }
437
+ input,
438
+ select {
439
+ font-size: 12px;
440
+ min-width: 0;
441
+ }
442
+ table {
443
+ border-collapse: collapse;
444
+ width: 100%;
445
+ }
446
+ td,
447
+ th {
448
+ border-bottom: 1px solid rgba(0, 0, 0, 0.08);
449
+ padding: 1px;
450
+ }
451
+ td input {
452
+ width: 100%;
453
+ box-sizing: border-box;
454
+ }
455
+ h4 {
456
+ margin: 6px 0 2px;
457
+ font-size: 12px;
458
+ }
459
+ svg {
460
+ width: 100%;
461
+ height: 160px;
462
+ background: rgba(0, 0, 0, 0.02);
463
+ border: 1px solid rgba(0, 0, 0, 0.08);
464
+ }
465
+ .actions {
466
+ display: flex;
467
+ gap: 4px;
468
+ justify-content: flex-end;
469
+ }
470
+ `;
471
+ __decorate([
472
+ property({ attribute: false })
473
+ ], OxSlotLayoutEditor.prototype, "value", void 0);
474
+ __decorate([
475
+ property({ attribute: false })
476
+ ], OxSlotLayoutEditor.prototype, "options", void 0);
477
+ __decorate([
478
+ state()
479
+ ], OxSlotLayoutEditor.prototype, "draft", void 0);
480
+ __decorate([
481
+ state()
482
+ ], OxSlotLayoutEditor.prototype, "tier", void 0);
483
+ __decorate([
484
+ state()
485
+ ], OxSlotLayoutEditor.prototype, "dirty", void 0);
486
+ __decorate([
487
+ state()
488
+ ], OxSlotLayoutEditor.prototype, "presetKey", void 0);
489
+ __decorate([
490
+ state()
491
+ ], OxSlotLayoutEditor.prototype, "presetSize", void 0);
492
+ __decorate([
493
+ state()
494
+ ], OxSlotLayoutEditor.prototype, "presetParams", void 0);
495
+ OxSlotLayoutEditor = __decorate([
496
+ customElement('ox-slot-layout-editor')
497
+ ], OxSlotLayoutEditor);
498
+ export { OxSlotLayoutEditor };
499
+ //# sourceMappingURL=ox-slot-layout-editor.js.map