@operato/slot-layout-editor 10.29.56 → 10.29.58
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 +24 -0
- package/dist/src/issue-text.d.ts +7 -0
- package/dist/src/issue-text.js +46 -1
- package/dist/src/issue-text.js.map +1 -1
- package/dist/src/ox-slot-layout-editor.d.ts +7 -1
- package/dist/src/ox-slot-layout-editor.js +82 -28
- package/dist/src/ox-slot-layout-editor.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/translations/en.json +3 -1
- package/translations/es.json +3 -1
- package/translations/ja.json +4 -2
- package/translations/ko.json +7 -5
- package/translations/ms.json +3 -1
- package/translations/zh.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
### [10.29.58](https://github.com/hatiolab/operato/compare/v10.29.57...v10.29.58) (2026-10-27)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### :rocket: New Features
|
|
10
|
+
|
|
11
|
+
* **slot-layout-editor:** one line for a tier's slots past their face, host-set panel look ([9e7946a](https://github.com/hatiolab/operato/commit/9e7946a111e775ef40efdd88fc3d0ef5af613d8c))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### [10.29.57](https://github.com/hatiolab/operato/compare/v10.29.56...v10.29.57) (2026-10-27)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### :rocket: New Features
|
|
19
|
+
|
|
20
|
+
* **slot-layout-editor:** start with no numbers to type ([fe55219](https://github.com/hatiolab/operato/commit/fe552194b9a45ea7a3fe2b16427b8a3e9805eb62))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### :bug: Bug Fix
|
|
24
|
+
|
|
25
|
+
* **slot-layout-editor:** lengths are shown to three decimals ([66bd3af](https://github.com/hatiolab/operato/commit/66bd3af6e5df6860e89f3e706938081370607992))
|
|
26
|
+
* **slot-layout-editor:** words for ACCEPTS_EMPTY, things-scene ^10.1.102 ([ca42efb](https://github.com/hatiolab/operato/commit/ca42efb8b2feb6cc917febd5a0597f6a1360aef9))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
6
30
|
### [10.29.56](https://github.com/hatiolab/operato/compare/v10.29.55...v10.29.56) (2026-10-26)
|
|
7
31
|
|
|
8
32
|
|
package/dist/src/issue-text.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export declare const t: (key: string, fallback: string, opts?: Record<string, un
|
|
|
17
17
|
/** 자리 공간 경고의 기본 문구. 번역 키는 label.slot-layout.space.{코드} */
|
|
18
18
|
export declare const SPACE_TEXT: Record<SlotSpaceCode, string>;
|
|
19
19
|
export declare function spaceWarningText(warning: EditorIssue): string;
|
|
20
|
+
/** Slot numbers as few words: runs of numbers as 1~8, the rest one by one. */
|
|
21
|
+
export declare function slotsWords(slots: readonly (string | number)[]): string;
|
|
22
|
+
/**
|
|
23
|
+
* The space warnings to show: the slots of one tier that reach past their face said in one line. Eight slots past a
|
|
24
|
+
* face were eight red lines that said the same (figure modeller sweep 2026-10-27 M3).
|
|
25
|
+
*/
|
|
26
|
+
export declare function spaceWarningLines(warnings: readonly EditorIssue[]): string[];
|
|
20
27
|
/** 문제 하나를 화면 문구로. 앞에 값의 위치를 붙인다 */
|
|
21
28
|
export declare function issueText(issue: EditorIssue): string;
|
|
22
29
|
/** 문제 하나를 화면 문구로, 위치 없이. 칸 옆에 보일 때 쓴다 */
|
package/dist/src/issue-text.js
CHANGED
|
@@ -22,7 +22,7 @@ export const ISSUE_TEXT = {
|
|
|
22
22
|
SLOTS_REQUIRED: 'At least one slot is required',
|
|
23
23
|
SLOT_NUMBER_DUPLICATE: 'Slot {slotNo} is used twice',
|
|
24
24
|
NOT_PERSISTABLE: 'Cannot be saved ({value})',
|
|
25
|
-
|
|
25
|
+
ACCEPTS_EMPTY: 'The list of types it takes is empty; choose 「Any type」 to take any item'
|
|
26
26
|
};
|
|
27
27
|
export const t = (key, fallback, opts = {}) => i18next.t(`label.slot-layout.${key}`, { defaultValue: fallback, ...opts });
|
|
28
28
|
/** 자리 공간 경고의 기본 문구. 번역 키는 label.slot-layout.space.{코드} */
|
|
@@ -36,6 +36,51 @@ export function spaceWarningText(warning) {
|
|
|
36
36
|
const fallback = (_a = SPACE_TEXT[warning.code]) !== null && _a !== void 0 ? _a : warning.code;
|
|
37
37
|
return t(`space.${warning.code}`, fallback, { ...warning.params, interpolation: { escapeValue: false } });
|
|
38
38
|
}
|
|
39
|
+
/** Slot numbers as few words: runs of numbers as 1~8, the rest one by one. */
|
|
40
|
+
export function slotsWords(slots) {
|
|
41
|
+
const numbers = slots.map(Number);
|
|
42
|
+
if (!numbers.every(Number.isInteger))
|
|
43
|
+
return slots.join(', ');
|
|
44
|
+
const sorted = [...new Set(numbers)].sort((a, b) => a - b);
|
|
45
|
+
const runs = [];
|
|
46
|
+
for (let i = 0; i < sorted.length;) {
|
|
47
|
+
let j = i;
|
|
48
|
+
while (j + 1 < sorted.length && sorted[j + 1] === sorted[j] + 1)
|
|
49
|
+
j++;
|
|
50
|
+
runs.push(j - i >= 2 ? `${sorted[i]}~${sorted[j]}` : sorted.slice(i, j + 1).join(', '));
|
|
51
|
+
i = j + 1;
|
|
52
|
+
}
|
|
53
|
+
return runs.join(', ');
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The space warnings to show: the slots of one tier that reach past their face said in one line. Eight slots past a
|
|
57
|
+
* face were eight red lines that said the same (figure modeller sweep 2026-10-27 M3).
|
|
58
|
+
*/
|
|
59
|
+
export function spaceWarningLines(warnings) {
|
|
60
|
+
var _a, _b, _c, _d;
|
|
61
|
+
const outside = new Map();
|
|
62
|
+
const lines = [];
|
|
63
|
+
for (const w of warnings) {
|
|
64
|
+
if (w.code !== 'SLOT_OUTSIDE_ANCHOR') {
|
|
65
|
+
lines.push(spaceWarningText(w));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const tier = String((_b = (_a = w.params) === null || _a === void 0 ? void 0 : _a.tier) !== null && _b !== void 0 ? _b : '');
|
|
69
|
+
if (!outside.has(tier)) {
|
|
70
|
+
outside.set(tier, []);
|
|
71
|
+
lines.push({ tier });
|
|
72
|
+
}
|
|
73
|
+
outside.get(tier).push((_d = (_c = w.params) === null || _c === void 0 ? void 0 : _c.slot) !== null && _d !== void 0 ? _d : '');
|
|
74
|
+
}
|
|
75
|
+
return lines.map(line => {
|
|
76
|
+
if (typeof line === 'string')
|
|
77
|
+
return line;
|
|
78
|
+
const slots = outside.get(line.tier);
|
|
79
|
+
if (slots.length === 1)
|
|
80
|
+
return spaceWarningText({ code: 'SLOT_OUTSIDE_ANCHOR', path: '', params: { tier: line.tier, slot: slots[0] } });
|
|
81
|
+
return t('space.SLOT_OUTSIDE_ANCHOR_MANY', 'Tier {tier} slots {slots} reach past their face', { tier: line.tier, slots: slotsWords(slots), interpolation: { escapeValue: false } });
|
|
82
|
+
});
|
|
83
|
+
}
|
|
39
84
|
/** 문제 하나를 화면 문구로. 앞에 값의 위치를 붙인다 */
|
|
40
85
|
export function issueText(issue) {
|
|
41
86
|
var _a;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"issue-text.js","sourceRoot":"","sources":["../../src/issue-text.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license Copyright © HatioLab Inc. All rights reserved.\n *\n * 검증 문제(코드·경로·매개변수)를 화면 문구로. 번역 키는 label.slot-layout.issue.{코드} 이고,\n * 번역이 없으면 아래 영어 기본 문구를 쓴다. 호스트 고유 코드는 호스트가 번역을 넣거나 issueText 옵션으로 바꾼다.\n */\n\nimport { i18next } from '@operato/i18n'\nimport type { LayoutIssueCode, SlotSpaceCode } from '@hatiolab/things-scene'\n\n/** 문제 하나. 호스트 고유 코드도 같은 모양으로 온다 */\nexport interface EditorIssue {\n code: string\n path: string\n params?: Record<string, string | number>\n}\n\n/** things-scene 검증 코드의 기본 문구. 자리표시자가 중괄호 하나라서 문구에 중괄호 글자를 쓰지 않는다 */\nexport const ISSUE_TEXT: Record<LayoutIssueCode, string> = {\n TIERS_REQUIRED: 'At least one tier is required',\n MOUNT_INVALID: 'Must be on, in or hang',\n ANCHOR_REQUIRED: 'Choose a face',\n ANCHOR_UNKNOWN: 'This holder has no face {anchorId}',\n TIER_NUMBER_DUPLICATE: 'Tier {tierNo} is used twice',\n FINITE_NUMBER_REQUIRED: 'A number is required',\n POSITIVE_NUMBER_REQUIRED: 'A number greater than 0 is required',\n POSITIVE_INTEGER_REQUIRED: 'A whole number of 1 or more is required',\n ARRAY_REQUIRED: 'A list is required',\n OBJECT_REQUIRED: 'The value is not a layout',\n AXIS_INVALID: 'Must be X or Z',\n PATTERN_INVALID: 'Must be one, row, grid or listed',\n SLOTS_REQUIRED: 'At least one slot is required',\n SLOT_NUMBER_DUPLICATE: 'Slot {slotNo} is used twice',\n NOT_PERSISTABLE: 'Cannot be saved ({value})',\n ACCEPTS_REQUIRED: 'Choose at least one type this slot takes'\n}\n\nexport const t = (key: string, fallback: string, opts: Record<string, unknown> = {}) =>\n i18next.t(`label.slot-layout.${key}`, { defaultValue: fallback, ...opts })\n\n/** 자리 공간 경고의 기본 문구. 번역 키는 label.slot-layout.space.{코드} */\nexport const SPACE_TEXT: Record<SlotSpaceCode, string> = {\n SLOT_OVERLAP: 'Tier {tier} slot {slot} overlaps tier {otherTier} slot {otherSlot}',\n SLOT_OUTSIDE_ANCHOR: 'Tier {tier} slot {slot} reaches past its face',\n ANCHOR_SPACE_UNKNOWN: 'Face {anchorId} has no size: whether slots stay on it is not checked'\n}\n\nexport function spaceWarningText(warning: EditorIssue): string {\n const fallback = (SPACE_TEXT as Record<string, string>)[warning.code] ?? warning.code\n return t(`space.${warning.code}`, fallback, { ...warning.params, interpolation: { escapeValue: false } })\n}\n\n/** 문제 하나를 화면 문구로. 앞에 값의 위치를 붙인다 */\nexport function issueText(issue: EditorIssue): string {\n const fallback = (ISSUE_TEXT as Record<string, string>)[issue.code] ?? issue.code\n const text = t(`issue.${issue.code}`, fallback, { ...issue.params, interpolation: { escapeValue: false } })\n return issue.path ? `${issue.path}: ${text}` : text\n}\n\n/** 문제 하나를 화면 문구로, 위치 없이. 칸 옆에 보일 때 쓴다 */\nexport function issueMessage(issue: EditorIssue): string {\n const fallback = (ISSUE_TEXT as Record<string, string>)[issue.code] ?? issue.code\n return t(`issue.${issue.code}`, fallback, { ...issue.params, interpolation: { escapeValue: false } })\n}\n\n/** Tier 안의 경로 → 편집기 칸(data-field). 칸이 없는 경로(CUSTOM 자리 표 등)는 목록에 보인다 */\nconst FIELD_OF_PATH: Record<string, string> = {\n anchorId: 'anchor',\n mount: 'mount',\n position: 'position-x',\n 'position.x': 'position-x',\n 'position.y': 'position-y',\n 'position.z': 'position-z',\n rotation: 'tier-rotation',\n layout: 'pattern',\n 'layout.pattern': 'pattern',\n 'layout.count': 'count',\n 'layout.pitch': 'pitch',\n 'layout.pitch.x': 'pitch-x',\n 'layout.pitch.z': 'pitch-z',\n 'layout.axis': 'axis',\n 'layout.origin': 'origin',\n 'layout.origin.x': 'origin-x',\n 'layout.origin.z': 'origin-z',\n 'layout.rows': 'rows',\n 'layout.cols': 'cols',\n 'layout.order': 'order',\n 'layout.slot': 'allowed-width',\n 'layout.slot.allowed': 'allowed-width',\n 'layout.slot.allowed.width': 'allowed-width',\n 'layout.slot.allowed.depth': 'allowed-depth',\n 'layout.slot.allowed.height': 'allowed-height',\n 'layout.slot.stackLimit': 'stack-limit',\n 'layout.slot.rotation': 'rotation',\n 'layout.slot.accepts': 'accepts'\n}\n\n/** 문제가 어느 Tier(목록 순서)의 어느 칸인지. Tier 밖이거나 칸이 없으면 field 없음 */\nexport function issueField(issue: EditorIssue): { tierIndex?: number; field?: string } {\n const m = /^tiers\\[(\\d+)\\](?:\\.(.+))?$/.exec(issue.path ?? '')\n if (!m) return {}\n return { tierIndex: Number(m[1]), field: m[2] ? FIELD_OF_PATH[m[2]] : undefined }\n}\n\n/** 실린 아이템 판정 까닭의 기본 문구. 번역 키는 label.slot-layout.fit.{까닭} */\nexport const FIT_TEXT: Record<string, string> = {\n SLOT_NOT_FOUND: 'Slot gone: kept, unplaced',\n TYPE_NOT_ACCEPTED: 'Type not taken here',\n WIDTH_EXCEEDED: 'Wider than the slot',\n DEPTH_EXCEEDED: 'Deeper than the slot',\n HEIGHT_EXCEEDED: 'Taller than the slot',\n STACK_LIMIT_EXCEEDED: 'Over the stack limit',\n SPEC_UNKNOWN: 'Size unknown: not judged'\n}\n\nexport function fitText(reason: string): string {\n return t(`fit.${reason}`, FIT_TEXT[reason] ?? reason)\n}\n\n/** 면에 맞출 수 없는 까닭의 기본 문구. 번역 키는 label.slot-layout.fit-face.{까닭} */\nexport const FIT_FACE_TEXT: Record<string, string> = {\n FACE_UNKNOWN: 'The face size is not known',\n PATTERN_UNSUPPORTED: 'Not for listed slots',\n ROTATION_UNSUPPORTED: 'Only for turns of 90°',\n SLOT_WIDER_THAN_FACE: 'A slot is larger than the face',\n SLOTS_DO_NOT_FIT: 'The slots do not fit on the face'\n}\n\nexport function fitFaceText(reason: string): string {\n return t(`fit-face.${reason}`, FIT_FACE_TEXT[reason] ?? reason)\n}\n"]}
|
|
1
|
+
{"version":3,"file":"issue-text.js","sourceRoot":"","sources":["../../src/issue-text.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAUvC,oEAAoE;AACpE,MAAM,CAAC,MAAM,UAAU,GAAoC;IACzD,cAAc,EAAE,+BAA+B;IAC/C,aAAa,EAAE,wBAAwB;IACvC,eAAe,EAAE,eAAe;IAChC,cAAc,EAAE,oCAAoC;IACpD,qBAAqB,EAAE,6BAA6B;IACpD,sBAAsB,EAAE,sBAAsB;IAC9C,wBAAwB,EAAE,qCAAqC;IAC/D,yBAAyB,EAAE,yCAAyC;IACpE,cAAc,EAAE,oBAAoB;IACpC,eAAe,EAAE,2BAA2B;IAC5C,YAAY,EAAE,gBAAgB;IAC9B,eAAe,EAAE,kCAAkC;IACnD,cAAc,EAAE,+BAA+B;IAC/C,qBAAqB,EAAE,6BAA6B;IACpD,eAAe,EAAE,2BAA2B;IAC5C,aAAa,EAAE,yEAAyE;CACzF,CAAA;AAED,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,GAAW,EAAE,QAAgB,EAAE,OAAgC,EAAE,EAAE,EAAE,CACrF,OAAO,CAAC,CAAC,CAAC,qBAAqB,GAAG,EAAE,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC,CAAA;AAE5E,0DAA0D;AAC1D,MAAM,CAAC,MAAM,UAAU,GAAkC;IACvD,YAAY,EAAE,oEAAoE;IAClF,mBAAmB,EAAE,+CAA+C;IACpE,oBAAoB,EAAE,sEAAsE;CAC7F,CAAA;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAoB;;IACnD,MAAM,QAAQ,GAAG,MAAC,UAAqC,CAAC,OAAO,CAAC,IAAI,CAAC,mCAAI,OAAO,CAAC,IAAI,CAAA;IACrF,OAAO,CAAC,CAAC,SAAS,OAAO,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;AAC3G,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,UAAU,CAAC,KAAmC;IAC5D,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACjC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC7D,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC1D,MAAM,IAAI,GAAa,EAAE,CAAA;IACzB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,GAAI,CAAC;QACpC,IAAI,CAAC,GAAG,CAAC,CAAA;QACT,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAE,GAAG,CAAC;YAAE,CAAC,EAAE,CAAA;QACrE,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;QACvF,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACX,CAAC;IACD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACxB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,QAAgC;;IAChE,MAAM,OAAO,GAAG,IAAI,GAAG,EAA+B,CAAA;IACtD,MAAM,KAAK,GAAkC,EAAE,CAAA;IAC/C,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACzB,IAAI,CAAC,CAAC,IAAI,KAAK,qBAAqB,EAAE,CAAC;YACrC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,CAAA;YAC/B,SAAQ;QACV,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAA,MAAA,CAAC,CAAC,MAAM,0CAAE,IAAI,mCAAI,EAAE,CAAC,CAAA;QACzC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACvB,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;YACrB,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAA;QACtB,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,IAAI,CAAC,MAAA,MAAA,CAAC,CAAC,MAAM,0CAAE,IAAI,mCAAI,EAAE,CAAC,CAAA;IAC/C,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE;QACtB,IAAI,OAAO,IAAI,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAA;QACzC,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAE,CAAA;QACrC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,gBAAgB,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAE,EAAE,EAAE,CAAC,CAAA;QACxI,OAAO,CAAC,CAAC,gCAAgC,EAAE,iDAAiD,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,UAAU,CAAC,KAAK,CAAC,EAAE,aAAa,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;IACrL,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,mCAAmC;AACnC,MAAM,UAAU,SAAS,CAAC,KAAkB;;IAC1C,MAAM,QAAQ,GAAG,MAAC,UAAqC,CAAC,KAAK,CAAC,IAAI,CAAC,mCAAI,KAAK,CAAC,IAAI,CAAA;IACjF,MAAM,IAAI,GAAG,CAAC,CAAC,SAAS,KAAK,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;IAC3G,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;AACrD,CAAC;AAED,yCAAyC;AACzC,MAAM,UAAU,YAAY,CAAC,KAAkB;;IAC7C,MAAM,QAAQ,GAAG,MAAC,UAAqC,CAAC,KAAK,CAAC,IAAI,CAAC,mCAAI,KAAK,CAAC,IAAI,CAAA;IACjF,OAAO,CAAC,CAAC,SAAS,KAAK,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,KAAK,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,CAAC,CAAA;AACvG,CAAC;AAED,uEAAuE;AACvE,MAAM,aAAa,GAA2B;IAC5C,QAAQ,EAAE,QAAQ;IAClB,KAAK,EAAE,OAAO;IACd,QAAQ,EAAE,YAAY;IACtB,YAAY,EAAE,YAAY;IAC1B,YAAY,EAAE,YAAY;IAC1B,YAAY,EAAE,YAAY;IAC1B,QAAQ,EAAE,eAAe;IACzB,MAAM,EAAE,SAAS;IACjB,gBAAgB,EAAE,SAAS;IAC3B,cAAc,EAAE,OAAO;IACvB,cAAc,EAAE,OAAO;IACvB,gBAAgB,EAAE,SAAS;IAC3B,gBAAgB,EAAE,SAAS;IAC3B,aAAa,EAAE,MAAM;IACrB,eAAe,EAAE,QAAQ;IACzB,iBAAiB,EAAE,UAAU;IAC7B,iBAAiB,EAAE,UAAU;IAC7B,aAAa,EAAE,MAAM;IACrB,aAAa,EAAE,MAAM;IACrB,cAAc,EAAE,OAAO;IACvB,aAAa,EAAE,eAAe;IAC9B,qBAAqB,EAAE,eAAe;IACtC,2BAA2B,EAAE,eAAe;IAC5C,2BAA2B,EAAE,eAAe;IAC5C,4BAA4B,EAAE,gBAAgB;IAC9C,wBAAwB,EAAE,aAAa;IACvC,sBAAsB,EAAE,UAAU;IAClC,qBAAqB,EAAE,SAAS;CACjC,CAAA;AAED,4DAA4D;AAC5D,MAAM,UAAU,UAAU,CAAC,KAAkB;;IAC3C,MAAM,CAAC,GAAG,6BAA6B,CAAC,IAAI,CAAC,MAAA,KAAK,CAAC,IAAI,mCAAI,EAAE,CAAC,CAAA;IAC9D,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAA;IACjB,OAAO,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,CAAA;AACnF,CAAC;AAED,4DAA4D;AAC5D,MAAM,CAAC,MAAM,QAAQ,GAA2B;IAC9C,cAAc,EAAE,2BAA2B;IAC3C,iBAAiB,EAAE,qBAAqB;IACxC,cAAc,EAAE,qBAAqB;IACrC,cAAc,EAAE,sBAAsB;IACtC,eAAe,EAAE,sBAAsB;IACvC,oBAAoB,EAAE,sBAAsB;IAC5C,YAAY,EAAE,0BAA0B;CACzC,CAAA;AAED,MAAM,UAAU,OAAO,CAAC,MAAc;;IACpC,OAAO,CAAC,CAAC,OAAO,MAAM,EAAE,EAAE,MAAA,QAAQ,CAAC,MAAM,CAAC,mCAAI,MAAM,CAAC,CAAA;AACvD,CAAC;AAED,kEAAkE;AAClE,MAAM,CAAC,MAAM,aAAa,GAA2B;IACnD,YAAY,EAAE,4BAA4B;IAC1C,mBAAmB,EAAE,sBAAsB;IAC3C,oBAAoB,EAAE,uBAAuB;IAC7C,oBAAoB,EAAE,gCAAgC;IACtD,gBAAgB,EAAE,kCAAkC;CACrD,CAAA;AAED,MAAM,UAAU,WAAW,CAAC,MAAc;;IACxC,OAAO,CAAC,CAAC,YAAY,MAAM,EAAE,EAAE,MAAA,aAAa,CAAC,MAAM,CAAC,mCAAI,MAAM,CAAC,CAAA;AACjE,CAAC","sourcesContent":["/**\n * @license Copyright © HatioLab Inc. All rights reserved.\n *\n * 검증 문제(코드·경로·매개변수)를 화면 문구로. 번역 키는 label.slot-layout.issue.{코드} 이고,\n * 번역이 없으면 아래 영어 기본 문구를 쓴다. 호스트 고유 코드는 호스트가 번역을 넣거나 issueText 옵션으로 바꾼다.\n */\n\nimport { i18next } from '@operato/i18n'\nimport type { LayoutIssueCode, SlotSpaceCode } from '@hatiolab/things-scene'\n\n/** 문제 하나. 호스트 고유 코드도 같은 모양으로 온다 */\nexport interface EditorIssue {\n code: string\n path: string\n params?: Record<string, string | number>\n}\n\n/** things-scene 검증 코드의 기본 문구. 자리표시자가 중괄호 하나라서 문구에 중괄호 글자를 쓰지 않는다 */\nexport const ISSUE_TEXT: Record<LayoutIssueCode, string> = {\n TIERS_REQUIRED: 'At least one tier is required',\n MOUNT_INVALID: 'Must be on, in or hang',\n ANCHOR_REQUIRED: 'Choose a face',\n ANCHOR_UNKNOWN: 'This holder has no face {anchorId}',\n TIER_NUMBER_DUPLICATE: 'Tier {tierNo} is used twice',\n FINITE_NUMBER_REQUIRED: 'A number is required',\n POSITIVE_NUMBER_REQUIRED: 'A number greater than 0 is required',\n POSITIVE_INTEGER_REQUIRED: 'A whole number of 1 or more is required',\n ARRAY_REQUIRED: 'A list is required',\n OBJECT_REQUIRED: 'The value is not a layout',\n AXIS_INVALID: 'Must be X or Z',\n PATTERN_INVALID: 'Must be one, row, grid or listed',\n SLOTS_REQUIRED: 'At least one slot is required',\n SLOT_NUMBER_DUPLICATE: 'Slot {slotNo} is used twice',\n NOT_PERSISTABLE: 'Cannot be saved ({value})',\n ACCEPTS_EMPTY: 'The list of types it takes is empty; choose 「Any type」 to take any item'\n}\n\nexport const t = (key: string, fallback: string, opts: Record<string, unknown> = {}) =>\n i18next.t(`label.slot-layout.${key}`, { defaultValue: fallback, ...opts })\n\n/** 자리 공간 경고의 기본 문구. 번역 키는 label.slot-layout.space.{코드} */\nexport const SPACE_TEXT: Record<SlotSpaceCode, string> = {\n SLOT_OVERLAP: 'Tier {tier} slot {slot} overlaps tier {otherTier} slot {otherSlot}',\n SLOT_OUTSIDE_ANCHOR: 'Tier {tier} slot {slot} reaches past its face',\n ANCHOR_SPACE_UNKNOWN: 'Face {anchorId} has no size: whether slots stay on it is not checked'\n}\n\nexport function spaceWarningText(warning: EditorIssue): string {\n const fallback = (SPACE_TEXT as Record<string, string>)[warning.code] ?? warning.code\n return t(`space.${warning.code}`, fallback, { ...warning.params, interpolation: { escapeValue: false } })\n}\n\n/** Slot numbers as few words: runs of numbers as 1~8, the rest one by one. */\nexport function slotsWords(slots: readonly (string | number)[]): string {\n const numbers = slots.map(Number)\n if (!numbers.every(Number.isInteger)) return slots.join(', ')\n const sorted = [...new Set(numbers)].sort((a, b) => a - b)\n const runs: string[] = []\n for (let i = 0; i < sorted.length; ) {\n let j = i\n while (j + 1 < sorted.length && sorted[j + 1] === sorted[j]! + 1) j++\n runs.push(j - i >= 2 ? `${sorted[i]}~${sorted[j]}` : sorted.slice(i, j + 1).join(', '))\n i = j + 1\n }\n return runs.join(', ')\n}\n\n/**\n * The space warnings to show: the slots of one tier that reach past their face said in one line. Eight slots past a\n * face were eight red lines that said the same (figure modeller sweep 2026-10-27 M3).\n */\nexport function spaceWarningLines(warnings: readonly EditorIssue[]): string[] {\n const outside = new Map<string, (string | number)[]>()\n const lines: (string | { tier: string })[] = []\n for (const w of warnings) {\n if (w.code !== 'SLOT_OUTSIDE_ANCHOR') {\n lines.push(spaceWarningText(w))\n continue\n }\n const tier = String(w.params?.tier ?? '')\n if (!outside.has(tier)) {\n outside.set(tier, [])\n lines.push({ tier })\n }\n outside.get(tier)!.push(w.params?.slot ?? '')\n }\n return lines.map(line => {\n if (typeof line === 'string') return line\n const slots = outside.get(line.tier)!\n if (slots.length === 1) return spaceWarningText({ code: 'SLOT_OUTSIDE_ANCHOR', path: '', params: { tier: line.tier, slot: slots[0]! } })\n return t('space.SLOT_OUTSIDE_ANCHOR_MANY', 'Tier {tier} slots {slots} reach past their face', { tier: line.tier, slots: slotsWords(slots), interpolation: { escapeValue: false } })\n })\n}\n\n/** 문제 하나를 화면 문구로. 앞에 값의 위치를 붙인다 */\nexport function issueText(issue: EditorIssue): string {\n const fallback = (ISSUE_TEXT as Record<string, string>)[issue.code] ?? issue.code\n const text = t(`issue.${issue.code}`, fallback, { ...issue.params, interpolation: { escapeValue: false } })\n return issue.path ? `${issue.path}: ${text}` : text\n}\n\n/** 문제 하나를 화면 문구로, 위치 없이. 칸 옆에 보일 때 쓴다 */\nexport function issueMessage(issue: EditorIssue): string {\n const fallback = (ISSUE_TEXT as Record<string, string>)[issue.code] ?? issue.code\n return t(`issue.${issue.code}`, fallback, { ...issue.params, interpolation: { escapeValue: false } })\n}\n\n/** Tier 안의 경로 → 편집기 칸(data-field). 칸이 없는 경로(CUSTOM 자리 표 등)는 목록에 보인다 */\nconst FIELD_OF_PATH: Record<string, string> = {\n anchorId: 'anchor',\n mount: 'mount',\n position: 'position-x',\n 'position.x': 'position-x',\n 'position.y': 'position-y',\n 'position.z': 'position-z',\n rotation: 'tier-rotation',\n layout: 'pattern',\n 'layout.pattern': 'pattern',\n 'layout.count': 'count',\n 'layout.pitch': 'pitch',\n 'layout.pitch.x': 'pitch-x',\n 'layout.pitch.z': 'pitch-z',\n 'layout.axis': 'axis',\n 'layout.origin': 'origin',\n 'layout.origin.x': 'origin-x',\n 'layout.origin.z': 'origin-z',\n 'layout.rows': 'rows',\n 'layout.cols': 'cols',\n 'layout.order': 'order',\n 'layout.slot': 'allowed-width',\n 'layout.slot.allowed': 'allowed-width',\n 'layout.slot.allowed.width': 'allowed-width',\n 'layout.slot.allowed.depth': 'allowed-depth',\n 'layout.slot.allowed.height': 'allowed-height',\n 'layout.slot.stackLimit': 'stack-limit',\n 'layout.slot.rotation': 'rotation',\n 'layout.slot.accepts': 'accepts'\n}\n\n/** 문제가 어느 Tier(목록 순서)의 어느 칸인지. Tier 밖이거나 칸이 없으면 field 없음 */\nexport function issueField(issue: EditorIssue): { tierIndex?: number; field?: string } {\n const m = /^tiers\\[(\\d+)\\](?:\\.(.+))?$/.exec(issue.path ?? '')\n if (!m) return {}\n return { tierIndex: Number(m[1]), field: m[2] ? FIELD_OF_PATH[m[2]] : undefined }\n}\n\n/** 실린 아이템 판정 까닭의 기본 문구. 번역 키는 label.slot-layout.fit.{까닭} */\nexport const FIT_TEXT: Record<string, string> = {\n SLOT_NOT_FOUND: 'Slot gone: kept, unplaced',\n TYPE_NOT_ACCEPTED: 'Type not taken here',\n WIDTH_EXCEEDED: 'Wider than the slot',\n DEPTH_EXCEEDED: 'Deeper than the slot',\n HEIGHT_EXCEEDED: 'Taller than the slot',\n STACK_LIMIT_EXCEEDED: 'Over the stack limit',\n SPEC_UNKNOWN: 'Size unknown: not judged'\n}\n\nexport function fitText(reason: string): string {\n return t(`fit.${reason}`, FIT_TEXT[reason] ?? reason)\n}\n\n/** 면에 맞출 수 없는 까닭의 기본 문구. 번역 키는 label.slot-layout.fit-face.{까닭} */\nexport const FIT_FACE_TEXT: Record<string, string> = {\n FACE_UNKNOWN: 'The face size is not known',\n PATTERN_UNSUPPORTED: 'Not for listed slots',\n ROTATION_UNSUPPORTED: 'Only for turns of 90°',\n SLOT_WIDER_THAN_FACE: 'A slot is larger than the face',\n SLOTS_DO_NOT_FIT: 'The slots do not fit on the face'\n}\n\nexport function fitFaceText(reason: string): string {\n return t(`fit-face.${reason}`, FIT_FACE_TEXT[reason] ?? reason)\n}\n"]}
|
|
@@ -34,8 +34,13 @@ export interface SlotLayoutEditorOptions<L extends SlotLayout = SlotLayout> {
|
|
|
34
34
|
live?: boolean;
|
|
35
35
|
/** 호스트가 주는 프리셋 */
|
|
36
36
|
presets?: SlotLayoutPreset<L>[];
|
|
37
|
-
/** 프리셋 크기 초기값 */
|
|
37
|
+
/** 프리셋 크기 초기값 (기준면의 폭·깊이와 기본 자리 높이). 다 주면 프리셋은 누르기만 하고 크기 칸은 접힌다 */
|
|
38
38
|
presetSize?: Partial<PresetSize>;
|
|
39
|
+
/**
|
|
40
|
+
* 레이아웃이 없을 때 이 프리셋으로 시작한 초안을 바로 보인다 (예: 'single' — 기준면 전체가 자리 하나). [적용]해야 들어간다.
|
|
41
|
+
* 크기를 다 알 때만 시작하고, 바로 적용하는 호스트(live)에서는 하지 않는다
|
|
42
|
+
*/
|
|
43
|
+
startFrom?: string;
|
|
39
44
|
/** 상태 문구 (값의 출처, 단위 상태 등) */
|
|
40
45
|
notes?: SlotLayoutEditorNote[];
|
|
41
46
|
/** 홀더가 주는 Anchor. 주면 Tier 는 이 중 하나를 고르고, 없는 Anchor 는 문제로 보인다. part·face 를 주면 면 이름으로 보인다 */
|
|
@@ -83,6 +88,7 @@ export declare class OxSlotLayoutEditor extends OxSlotLayoutEditor_base {
|
|
|
83
88
|
willUpdate(changed: Map<string, unknown>): void;
|
|
84
89
|
/** 편집 중인 값을 버리고 개별 설정(없으면 호스트 기본값)에서 다시 시작한다. 고른 Tier 는 남아 있으면 그대로 */
|
|
85
90
|
reset(): void;
|
|
91
|
+
private _startFromHost;
|
|
86
92
|
get issues(): EditorIssue[];
|
|
87
93
|
/** 자리끼리 겹치거나 면 밖으로 나가는 설정, 면 크기를 몰라 확인하지 못한 Anchor. 적용을 막지 않는다 */
|
|
88
94
|
get warnings(): EditorIssue[];
|
|
@@ -19,12 +19,13 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|
|
19
19
|
import { i18next, localize } from '@operato/i18n';
|
|
20
20
|
import { persistenceIssues, rotatedFootprint, slotSpaceWarnings, validateSlotLayout } from '@hatiolab/things-scene';
|
|
21
21
|
import { addCustomSlot, addressImpact, anchorFaceSlots, applyPreset, copyTier, fitTierToFace, heldItemStatuses, pitchToPoint, removeCustomSlot, removeTier, setTierPattern, updatePattern, updateSlotSpec, updateTier } from './draft.js';
|
|
22
|
-
import { fitFaceText, fitText, issueField, issueMessage, issueText as defaultIssueText, ISSUE_TEXT,
|
|
22
|
+
import { fitFaceText, fitText, issueField, issueMessage, issueText as defaultIssueText, ISSUE_TEXT, spaceWarningLines, t } from './issue-text.js';
|
|
23
23
|
const num = (e) => {
|
|
24
24
|
const n = e.target.valueAsNumber;
|
|
25
25
|
return Number.isFinite(n) ? n : NaN;
|
|
26
26
|
};
|
|
27
|
-
|
|
27
|
+
/* 보이는 값: 길이는 소수 셋째 자리까지 — 피규어에서 잰 면은 부동소수 잡음을 싣는다(759.9999998556777 → 760) */
|
|
28
|
+
const shown = (v) => (v === undefined || v === null || Number.isNaN(v) ? '' : typeof v === 'number' ? String(Number(v.toFixed(3))) : String(v));
|
|
28
29
|
const snap = (v) => Math.round(v / 10) * 10;
|
|
29
30
|
const ERROR = '#c62828';
|
|
30
31
|
const SLOT = '#1976d2';
|
|
@@ -119,6 +120,7 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
|
|
|
119
120
|
// 값도 기본값도 없는 호스트도 프리셋 크기는 준다. 크기가 실제로 바뀔 때만 받고, 편집 중인 초안은 그대로 둔다
|
|
120
121
|
if (JSON.stringify((_h = before === null || before === void 0 ? void 0 : before.presetSize) !== null && _h !== void 0 ? _h : {}) !== JSON.stringify((_k = (_j = this.options) === null || _j === void 0 ? void 0 : _j.presetSize) !== null && _k !== void 0 ? _k : {}))
|
|
121
122
|
this.presetSize = { ...((_m = (_l = this.options) === null || _l === void 0 ? void 0 : _l.presetSize) !== null && _m !== void 0 ? _m : {}) };
|
|
123
|
+
this._startFromHost();
|
|
122
124
|
}
|
|
123
125
|
/** 편집 중인 값을 버리고 개별 설정(없으면 호스트 기본값)에서 다시 시작한다. 고른 Tier 는 남아 있으면 그대로 */
|
|
124
126
|
reset() {
|
|
@@ -133,6 +135,21 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
|
|
|
133
135
|
this.presetSize = { ...((_k = (_j = this.options) === null || _j === void 0 ? void 0 : _j.presetSize) !== null && _k !== void 0 ? _k : {}) };
|
|
134
136
|
if (!((_m = (_l = this.options) === null || _l === void 0 ? void 0 : _l.presets) === null || _m === void 0 ? void 0 : _m.some(p => p.key === this.presetKey)))
|
|
135
137
|
this.presetKey = (_r = (_q = (_p = (_o = this.options) === null || _o === void 0 ? void 0 : _o.presets) === null || _p === void 0 ? void 0 : _p[0]) === null || _q === void 0 ? void 0 : _q.key) !== null && _r !== void 0 ? _r : '';
|
|
138
|
+
this._startFromHost();
|
|
139
|
+
}
|
|
140
|
+
/* 레이아웃이 없으면 호스트가 고른 프리셋(startFrom)으로 시작한 초안을 보인다. 저장은 [적용] 때 — 끼어들어 쓰지 않는다 */
|
|
141
|
+
_startFromHost() {
|
|
142
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
143
|
+
const key = (_a = this.options) === null || _a === void 0 ? void 0 : _a.startFrom;
|
|
144
|
+
if (this.draft || this.value || ((_b = this.options) === null || _b === void 0 ? void 0 : _b.effective) || ((_c = this.options) === null || _c === void 0 ? void 0 : _c.live) || !key)
|
|
145
|
+
return;
|
|
146
|
+
const preset = (_e = (_d = this.options) === null || _d === void 0 ? void 0 : _d.presets) === null || _e === void 0 ? void 0 : _e.find(p => p.key === key);
|
|
147
|
+
const built = preset && !((_f = preset.params) === null || _f === void 0 ? void 0 : _f.length) ? applyPreset(preset, this.presetSize, {}) : undefined;
|
|
148
|
+
if (!(built === null || built === void 0 ? void 0 : built.layout))
|
|
149
|
+
return;
|
|
150
|
+
this.draft = structuredClone(built.layout);
|
|
151
|
+
this.dirty = true;
|
|
152
|
+
this.tier = (_h = (_g = this.draft.tiers[0]) === null || _g === void 0 ? void 0 : _g.tierNo) !== null && _h !== void 0 ? _h : 1;
|
|
136
153
|
}
|
|
137
154
|
get issues() {
|
|
138
155
|
var _a, _b, _c, _d, _e;
|
|
@@ -236,29 +253,57 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
|
|
|
236
253
|
return nothing;
|
|
237
254
|
const preset = (_c = presets.find(p => p.key === this.presetKey)) !== null && _c !== void 0 ? _c : presets[0];
|
|
238
255
|
const built = applyPreset(preset, this.presetSize, this.presetParams);
|
|
256
|
+
const positive = (v) => typeof v === 'number' && Number.isFinite(v) && v > 0;
|
|
257
|
+
const sized = positive(this.presetSize.width) && positive(this.presetSize.depth) && positive(this.presetSize.slotHeight);
|
|
239
258
|
const numberRow = (label, value, onChange, key) => html `
|
|
240
259
|
<label class="l">${label}</label>
|
|
241
260
|
<input type="number" data-preset=${key} .value=${shown(value)} @change=${(e) => onChange(num(e))} />
|
|
242
261
|
`;
|
|
262
|
+
/* 값을 더 묻지 않는 프리셋은 누르면 바로 시작한다. 값을 묻는 프리셋(행 수 등)은 그 칸과 [시작]을 연다 */
|
|
263
|
+
const choose = (p) => {
|
|
264
|
+
var _a;
|
|
265
|
+
this.presetKey = p.key;
|
|
266
|
+
if ((_a = p.params) === null || _a === void 0 ? void 0 : _a.length)
|
|
267
|
+
return;
|
|
268
|
+
const made = applyPreset(p, this.presetSize, {});
|
|
269
|
+
if (made.layout)
|
|
270
|
+
this._startFrom(made.layout);
|
|
271
|
+
};
|
|
243
272
|
return html `
|
|
244
273
|
<details data-section="preset" ?open=${!this.draft}>
|
|
245
274
|
<summary>${t('preset', 'Start again from a preset')}</summary>
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
<button data-action="start-preset" ?disabled=${!built.layout} @click=${() => built.layout && this._startFrom(built.layout)}>
|
|
259
|
-
${t('start-from-preset', 'Start from preset')}
|
|
260
|
-
</button>
|
|
275
|
+
<div class="tiers" data-preset="kind" role="group">
|
|
276
|
+
${presets.map(p => {
|
|
277
|
+
var _a;
|
|
278
|
+
return html `<button
|
|
279
|
+
data-preset-key=${p.key}
|
|
280
|
+
aria-pressed=${((_a = p.params) === null || _a === void 0 ? void 0 : _a.length) && p.key === preset.key ? 'true' : 'false'}
|
|
281
|
+
?disabled=${!sized}
|
|
282
|
+
@click=${() => choose(p)}
|
|
283
|
+
>
|
|
284
|
+
${t(`preset.${p.key}`, p.key)}
|
|
285
|
+
</button>`;
|
|
286
|
+
})}
|
|
261
287
|
</div>
|
|
288
|
+
${((_d = preset.params) === null || _d === void 0 ? void 0 : _d.length)
|
|
289
|
+
? html `<section>
|
|
290
|
+
${preset.params.map(p => numberRow(t(`preset-param.${p.key}`, p.key), this.presetParams[p.key], n => (this.presetParams = { ...this.presetParams, [p.key]: n }), p.key))}
|
|
291
|
+
</section>
|
|
292
|
+
<div class="actions">
|
|
293
|
+
<button data-action="start-preset" ?disabled=${!built.layout} @click=${() => built.layout && this._startFrom(built.layout)}>
|
|
294
|
+
${t('start-from-preset', 'Start from preset')}
|
|
295
|
+
</button>
|
|
296
|
+
</div>`
|
|
297
|
+
: nothing}
|
|
298
|
+
<details data-section="preset-size" ?open=${!sized}>
|
|
299
|
+
<summary>${t('preset-size', 'Size (enter by hand)')}</summary>
|
|
300
|
+
<section>
|
|
301
|
+
${numberRow(t('width', 'Width (X)'), this.presetSize.width, n => (this.presetSize = { ...this.presetSize, width: n }), 'width')}
|
|
302
|
+
${numberRow(t('depth', 'Depth (Z)'), this.presetSize.depth, n => (this.presetSize = { ...this.presetSize, depth: n }), 'depth')}
|
|
303
|
+
${numberRow(t('slot-height', 'Slot height'), this.presetSize.slotHeight, n => (this.presetSize = { ...this.presetSize, slotHeight: n }), 'slotHeight')}
|
|
304
|
+
</section>
|
|
305
|
+
</details>
|
|
306
|
+
<div class="hint">${t('preset-note', 'Pitch and slot size are split evenly as starting values. Confirm them against the real equipment.')}</div>
|
|
262
307
|
</details>
|
|
263
308
|
`;
|
|
264
309
|
}
|
|
@@ -327,8 +372,10 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
|
|
|
327
372
|
const a = (_b = (_a = this.options) === null || _a === void 0 ? void 0 : _a.anchors) === null || _b === void 0 ? void 0 : _b.find(x => x.id === id);
|
|
328
373
|
if (a === null || a === void 0 ? void 0 : a.label)
|
|
329
374
|
return a.label;
|
|
375
|
+
// 면 이름이 이미 부품을 말한다: id 가 부품 이름과 같으면 다시 적지 않는다(「tub · tub 윗면」, figure 점검 2026-10-27 M5)
|
|
376
|
+
const face = t(`face-${(_c = a === null || a === void 0 ? void 0 : a.face) !== null && _c !== void 0 ? _c : 'top'}`, `{part} ${(_d = a === null || a === void 0 ? void 0 : a.face) !== null && _d !== void 0 ? _d : 'top'}`, { part: a === null || a === void 0 ? void 0 : a.part });
|
|
330
377
|
if (a === null || a === void 0 ? void 0 : a.part)
|
|
331
|
-
return
|
|
378
|
+
return a.part === id ? face : `${id} · ${face}`;
|
|
332
379
|
return id;
|
|
333
380
|
}
|
|
334
381
|
_unit() {
|
|
@@ -420,7 +467,7 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
|
|
|
420
467
|
<div class="caption">
|
|
421
468
|
<span data-face-caption>
|
|
422
469
|
${face
|
|
423
|
-
? html `${this._anchorName(tier.anchorId)} · ${face.width} × ${face.depth}${this._unit()}`
|
|
470
|
+
? html `${this._anchorName(tier.anchorId)} · ${shown(face.width)} × ${shown(face.depth)}${this._unit()}`
|
|
424
471
|
: html `<span class="error">${t('face-unknown', 'Face size unknown: whether slots stay on it is not checked')}</span>`}
|
|
425
472
|
</span>
|
|
426
473
|
<span>${t('preview-axes', 'Right = +X, up = −Z')}</span>
|
|
@@ -433,7 +480,7 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
|
|
|
433
480
|
const warnings = this.warnings.filter(w => { var _a; return !(w.code === 'ANCHOR_SPACE_UNKNOWN' && !faceKnown && ((_a = w.params) === null || _a === void 0 ? void 0 : _a.anchorId) === anchorId); });
|
|
434
481
|
if (!warnings.length)
|
|
435
482
|
return nothing;
|
|
436
|
-
return html `<div class="error" data-warnings>${warnings.map(
|
|
483
|
+
return html `<div class="error" data-warnings>${spaceWarningLines(warnings).map(line => html `<div>· ${line}</div>`)}</div>`;
|
|
437
484
|
}
|
|
438
485
|
/**
|
|
439
486
|
* 실린 아이템이 편집 중인 레이아웃에서 어떻게 되는지. 자리가 없어지면 미배치로 남고(지우거나 옮기지 않는다), 받는 타입·규격은
|
|
@@ -708,7 +755,7 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
|
|
|
708
755
|
var _a, _b, _c;
|
|
709
756
|
const tier = draft.tiers.find(x => x.tierNo === tierNo);
|
|
710
757
|
const slots = [...tier.layout.slots].sort((a, b) => a.slotNo - b.slotNo);
|
|
711
|
-
const cell = (slotNo, value, patch) => html `<td><input type="number" .value=${
|
|
758
|
+
const cell = (slotNo, value, patch) => html `<td><input type="number" .value=${shown(value)} @change=${(e) => this._edit(updateSlotSpec(draft, tierNo, patch(e), slotNo))} /></td>`;
|
|
712
759
|
return html `
|
|
713
760
|
<h4>${t('custom-slots', 'Slots')}</h4>
|
|
714
761
|
<table>
|
|
@@ -749,18 +796,24 @@ let OxSlotLayoutEditor = class OxSlotLayoutEditor extends localize(i18next)(LitE
|
|
|
749
796
|
`;
|
|
750
797
|
}
|
|
751
798
|
};
|
|
799
|
+
/*
|
|
800
|
+
호스트가 자기 패널의 모양을 줄 수 있다(figure 점검 2026-10-27 M2): 글자 크기, 라벨 열과 정렬, 입력칸의 높이·테두리.
|
|
801
|
+
--slot-layout-font-size · --slot-layout-label-column · --slot-layout-label-align ·
|
|
802
|
+
--slot-layout-field-border · --slot-layout-field-border-bottom · --slot-layout-field-radius · --slot-layout-field-background.
|
|
803
|
+
주지 않으면 지금 모양 그대로다(--property-editor-* 를 따른다).
|
|
804
|
+
*/
|
|
752
805
|
OxSlotLayoutEditor.styles = css `
|
|
753
806
|
:host {
|
|
754
807
|
display: flex;
|
|
755
808
|
flex-direction: column;
|
|
756
809
|
gap: 8px;
|
|
757
810
|
min-width: 0;
|
|
758
|
-
font-size: 12px;
|
|
811
|
+
font-size: var(--slot-layout-font-size, 12px);
|
|
759
812
|
color: var(--property-editor-color, inherit);
|
|
760
813
|
}
|
|
761
814
|
section {
|
|
762
815
|
display: grid;
|
|
763
|
-
grid-template-columns: minmax(64px, 34%) minmax(0, 1fr);
|
|
816
|
+
grid-template-columns: var(--slot-layout-label-column, minmax(64px, 34%)) minmax(0, 1fr);
|
|
764
817
|
gap: 4px 6px;
|
|
765
818
|
align-items: center;
|
|
766
819
|
}
|
|
@@ -768,7 +821,7 @@ OxSlotLayoutEditor.styles = css `
|
|
|
768
821
|
summary {
|
|
769
822
|
grid-column: 1 / -1;
|
|
770
823
|
margin: 4px 0 0;
|
|
771
|
-
font-size: 12px;
|
|
824
|
+
font-size: var(--slot-layout-font-size, 12px);
|
|
772
825
|
font-weight: bold;
|
|
773
826
|
}
|
|
774
827
|
summary {
|
|
@@ -778,7 +831,7 @@ OxSlotLayoutEditor.styles = css `
|
|
|
778
831
|
margin-top: 4px;
|
|
779
832
|
}
|
|
780
833
|
label.l {
|
|
781
|
-
text-align: right;
|
|
834
|
+
text-align: var(--slot-layout-label-align, right);
|
|
782
835
|
font: var(--property-editor-label-font, inherit);
|
|
783
836
|
color: var(--property-editor-label-color, inherit);
|
|
784
837
|
}
|
|
@@ -813,9 +866,10 @@ OxSlotLayoutEditor.styles = css `
|
|
|
813
866
|
min-width: 0;
|
|
814
867
|
min-height: var(--property-editor-field-height, 24px);
|
|
815
868
|
padding: var(--property-editor-field-padding, 1px 4px);
|
|
816
|
-
border: var(--property-editor-border, 1px solid rgba(0, 0, 0, 0.2));
|
|
817
|
-
border-
|
|
818
|
-
|
|
869
|
+
border: var(--slot-layout-field-border, var(--property-editor-border, 1px solid rgba(0, 0, 0, 0.2)));
|
|
870
|
+
border-bottom: var(--slot-layout-field-border-bottom, var(--slot-layout-field-border, var(--property-editor-border, 1px solid rgba(0, 0, 0, 0.2))));
|
|
871
|
+
border-radius: var(--slot-layout-field-radius, var(--property-editor-border-radius, 4px));
|
|
872
|
+
background-color: var(--slot-layout-field-background, var(--property-editor-background, field));
|
|
819
873
|
color: inherit;
|
|
820
874
|
font: var(--property-editor-field-font, inherit);
|
|
821
875
|
outline: none;
|