@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 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
 
@@ -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
  /** 문제 하나를 화면 문구로, 위치 없이. 칸 옆에 보일 때 쓴다 */
@@ -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
- ACCEPTS_REQUIRED: 'Choose at least one type this slot takes'
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":"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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_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, spaceWarningText, t } from './issue-text.js';
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
- const shown = (v) => (v === undefined || v === null || Number.isNaN(v) ? '' : String(v));
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
- <section>
247
- <label class="l">${t('preset-kind', 'Kind')}</label>
248
- <select data-preset="kind" @change=${(e) => (this.presetKey = e.target.value)}>
249
- ${presets.map(p => html `<option value=${p.key} ?selected=${p.key === preset.key}>${t(`preset.${p.key}`, p.key)}</option>`)}
250
- </select>
251
- ${numberRow(t('width', 'Width (X)'), this.presetSize.width, n => (this.presetSize = { ...this.presetSize, width: n }), 'width')}
252
- ${numberRow(t('depth', 'Depth (Z)'), this.presetSize.depth, n => (this.presetSize = { ...this.presetSize, depth: n }), 'depth')}
253
- ${numberRow(t('slot-height', 'Slot height'), this.presetSize.slotHeight, n => (this.presetSize = { ...this.presetSize, slotHeight: n }), 'slotHeight')}
254
- ${((_d = preset.params) !== null && _d !== void 0 ? _d : []).map(p => numberRow(t(`preset-param.${p.key}`, p.key), this.presetParams[p.key], n => (this.presetParams = { ...this.presetParams, [p.key]: n }), p.key))}
255
- <div class="hint">${t('preset-note', 'Pitch and slot size are split evenly as starting values. Confirm them against the real equipment.')}</div>
256
- </section>
257
- <div class="actions">
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 `${id} · ${t(`face-${(_c = a.face) !== null && _c !== void 0 ? _c : 'top'}`, `{part} ${(_d = a.face) !== null && _d !== void 0 ? _d : 'top'}`, { part: a.part })}`;
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(w => html `<div>· ${spaceWarningText(w)}</div>`)}</div>`;
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=${String(value !== null && value !== void 0 ? value : '')} @change=${(e) => this._edit(updateSlotSpec(draft, tierNo, patch(e), slotNo))} /></td>`;
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-radius: var(--property-editor-border-radius, 4px);
818
- background-color: var(--property-editor-background, field);
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;