@fmsim/machine 2.0.1 → 3.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/agv.js +2 -3
  2. package/dist/agv.js.map +1 -1
  3. package/dist/carrier.js +53 -43
  4. package/dist/carrier.js.map +1 -1
  5. package/dist/crane.js +37 -2
  6. package/dist/crane.js.map +1 -1
  7. package/dist/editors/index.js +2 -0
  8. package/dist/editors/index.js.map +1 -1
  9. package/dist/editors/load-layout-host.js +96 -0
  10. package/dist/editors/load-layout-host.js.map +1 -0
  11. package/dist/editors/ox-load-layout-panel.js +124 -0
  12. package/dist/editors/ox-load-layout-panel.js.map +1 -0
  13. package/dist/factories/crane-3d.js +77 -18
  14. package/dist/factories/crane-3d.js.map +1 -1
  15. package/dist/factories/machine-3d.js +29 -52
  16. package/dist/factories/machine-3d.js.map +1 -1
  17. package/dist/features/capacity-gauge-mixin.js +2 -3
  18. package/dist/features/capacity-gauge-mixin.js.map +1 -1
  19. package/dist/features/load-layout-holder.js +77 -0
  20. package/dist/features/load-layout-holder.js.map +1 -0
  21. package/dist/features/mcs-status-default.js +30 -7
  22. package/dist/features/mcs-status-default.js.map +1 -1
  23. package/dist/features/mcs-status-mixin.js +3 -3
  24. package/dist/features/mcs-status-mixin.js.map +1 -1
  25. package/dist/index.js +5 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/mcs-carrier-holder.js +287 -82
  28. package/dist/mcs-carrier-holder.js.map +1 -1
  29. package/dist/oht.js +4 -0
  30. package/dist/oht.js.map +1 -1
  31. package/dist/port.js +6 -1
  32. package/dist/port.js.map +1 -1
  33. package/dist/scene/root-container-override.js +5 -11
  34. package/dist/scene/root-container-override.js.map +1 -1
  35. package/dist/shelf.js +4 -0
  36. package/dist/shelf.js.map +1 -1
  37. package/dist/shuttle.js +4 -0
  38. package/dist/shuttle.js.map +1 -1
  39. package/dist/stocker-carrier-bar.js +4 -4
  40. package/dist/stocker-carrier-bar.js.map +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/dist/utils/carrier-type-code.js +10 -0
  43. package/dist/utils/carrier-type-code.js.map +1 -0
  44. package/dist/utils/crane-forks.js +5 -0
  45. package/dist/utils/crane-forks.js.map +1 -0
  46. package/dist/utils/layout-issue-text.js +16 -0
  47. package/dist/utils/layout-issue-text.js.map +1 -0
  48. package/dist/utils/layout-issue.js +16 -0
  49. package/dist/utils/layout-issue.js.map +1 -0
  50. package/dist/utils/load-address.js +217 -0
  51. package/dist/utils/load-address.js.map +1 -0
  52. package/dist/utils/stocker-gauge.js +3 -2
  53. package/dist/utils/stocker-gauge.js.map +1 -1
  54. package/jest.config.js +9 -1
  55. package/package.json +9 -6
  56. package/src/agv.ts +2 -3
  57. package/src/carrier.ts +50 -41
  58. package/src/crane.ts +39 -2
  59. package/src/editors/index.ts +2 -0
  60. package/src/editors/load-layout-host.ts +121 -0
  61. package/src/editors/ox-load-layout-panel.ts +129 -0
  62. package/src/factories/crane-3d.ts +74 -20
  63. package/src/factories/machine-3d.ts +26 -49
  64. package/src/features/capacity-gauge-mixin.ts +2 -3
  65. package/src/features/load-layout-holder.ts +83 -0
  66. package/src/features/mcs-status-default.ts +29 -10
  67. package/src/features/mcs-status-mixin.ts +3 -7
  68. package/src/index.ts +7 -0
  69. package/src/mcs-carrier-holder.ts +319 -78
  70. package/src/oht.ts +5 -0
  71. package/src/port.ts +7 -1
  72. package/src/scene/root-container-override.ts +4 -14
  73. package/src/shelf.ts +5 -0
  74. package/src/shuttle.ts +5 -0
  75. package/src/stocker-carrier-bar.ts +4 -4
  76. package/src/utils/carrier-type-code.ts +9 -0
  77. package/src/utils/crane-forks.ts +4 -0
  78. package/src/utils/layout-issue-text.ts +18 -0
  79. package/src/utils/layout-issue.ts +41 -0
  80. package/src/utils/load-address.ts +270 -0
  81. package/src/utils/stocker-gauge.ts +3 -3
  82. package/translations/en.json +42 -1
  83. package/translations/ja.json +42 -1
  84. package/translations/ko.json +59 -18
  85. package/translations/ms.json +42 -1
  86. package/translations/zh.json +42 -1
  87. package/dist/carrier-manager.js +0 -191
  88. package/dist/carrier-manager.js.map +0 -1
  89. package/dist/features/animation-default.js +0 -6
  90. package/dist/features/animation-default.js.map +0 -1
  91. package/dist/utils/get-value-on-ranges.js +0 -21
  92. package/dist/utils/get-value-on-ranges.js.map +0 -1
  93. package/src/carrier-manager.ts +0 -231
  94. package/src/features/animation-default.ts +0 -13
  95. package/src/utils/get-value-on-ranges.ts +0 -23
@@ -0,0 +1,96 @@
1
+ /*
2
+ * fmsim 이 공통 편집기(@operato/slot-layout-editor)에 주는 것 (설계 02번 3.7.1절)
3
+ * Tier·Slot 편집, 검증, 미리보기, 적용 전 주소 영향은 공통 편집기가 한다. 기종 프리셋·실효 레이아웃·검증은 figure-model 의
4
+ * 것이다(ADR-0051 4.1절 결정 8). 이 패널은 도면 노드의 배치 덮어쓰기 화면이고, fmsim 은 자기 몫만 넘긴다:
5
+ * 프리셋 이름, 홀더가 주는 Anchor, 값의 출처·도면 단위 안내, 실시간 주소 읽기 설정과 그 검증, fmsim 고유 오류 문구.
6
+ * 화면 요소를 만들지 않는 순수 모듈이다 — 문구 함수(t)와 공통 문구 함수는 부르는 쪽이 준다.
7
+ */
8
+ import { figureSlotLayoutIssues, slotLayoutPreset, SLOT_LAYOUT_PRESET_KEYS, SLOT_LAYOUT_PRESET_PARAMS } from '@hatiolab/figure-model';
9
+ import { validateAddressing } from '../utils/load-address';
10
+ import { ISSUE_TEXT } from '../utils/layout-issue-text';
11
+ // ── 기종 프리셋 (3.7절, figure-model) ─────────────────────────────────────────
12
+ const PRESET_LABELS = {
13
+ single: 'Single slot',
14
+ agvTandem: 'AGV tandem (front/rear)',
15
+ agvParallel: 'AGV parallel (left/right)',
16
+ agv2x2: 'AGV 2×2',
17
+ bufferPort2x2: 'Buffer port 2×2',
18
+ multiBufferPort: 'Multi-buffer port (4)',
19
+ dualTierConveyor: 'Dual-tier conveyor',
20
+ craneTwinFork: 'Crane twin fork',
21
+ floorBlock: 'Floor block'
22
+ };
23
+ const PARAM_LABELS = {
24
+ upperElevation: 'Upper tier elevation (mm)',
25
+ rows: 'Rows',
26
+ cols: 'Columns',
27
+ stackLimit: 'Stack limit'
28
+ };
29
+ /** 공통 편집기의 프리셋 목록. 홀더 원점·포크 Anchor 위에 펼치고, 단일 자리는 홀더의 적재 방식으로 만든다 */
30
+ export function fmsimPresets(mount, t) {
31
+ return SLOT_LAYOUT_PRESET_KEYS.map(key => ({
32
+ key,
33
+ label: t(`preset-${key}`, PRESET_LABELS[key]),
34
+ params: SLOT_LAYOUT_PRESET_PARAMS[key].map(p => ({ key: p.key, label: t(`param-${p.key}`, PARAM_LABELS[p.key]), integer: p.integer })),
35
+ build: (size, params) => slotLayoutPreset(key, size, params, { mount })
36
+ }));
37
+ }
38
+ // ── 실시간 주소 읽기 (3.6절) ────────────────────────────────────────────────
39
+ export function setAddressing(layout, addressing) {
40
+ const next = structuredClone(layout);
41
+ if (addressing)
42
+ next.addressing = structuredClone(addressing);
43
+ else
44
+ delete next.addressing;
45
+ return next;
46
+ }
47
+ // ── 편집기 옵션 ─────────────────────────────────────────────────────────────
48
+ /** fmsim 고유 오류(주소 읽기 등)의 문구. fmsim 코드가 아니면 undefined — 공통 편집기 문구를 쓴다 */
49
+ export function fmsimIssueText(issue, t) {
50
+ const fallback = ISSUE_TEXT[issue.code];
51
+ if (!fallback)
52
+ return undefined;
53
+ const text = t(`issue.${issue.code}`, fallback, Object.assign(Object.assign({}, issue.params), { interpolation: { escapeValue: false } }));
54
+ return issue.path ? `${issue.path}: ${text}` : text;
55
+ }
56
+ /**
57
+ * 공통 편집기가 보는 것 밖의 검증: 실효 레이아웃이 덮어쓰기에 거는 figure 규칙(받는 타입은 생략하면 모든 아이템, 적으면
58
+ * 비지 않은 목록, ADR-0051 4.1절 결정 5·7)과 주소 읽기 설정. 공통 검증 결과는 편집기가 이미 보이므로 figure 고유 코드만 더한다.
59
+ */
60
+ export function holderLayoutIssues(layout, anchors) {
61
+ return [...figureSlotLayoutIssues(layout, anchors).filter(i => i.code === 'ACCEPTS_EMPTY'), ...validateAddressing(layout.addressing)];
62
+ }
63
+ /** 선택한 홀더(LoadLayoutHolder)에서 공통 편집기 옵션을 만든다. value 는 도면 노드의 개별 설정 */
64
+ export function holderEditorOptions(holder, value, t, commonText) {
65
+ var _a, _b, _c;
66
+ const sel = holder.effectiveLayout;
67
+ const mm = holder.mmPerUnit;
68
+ const state = holder.state || {};
69
+ const anchors = (_b = (_a = holder.slotAnchors) === null || _a === void 0 ? void 0 : _a.call(holder)) !== null && _b !== void 0 ? _b : [];
70
+ const source = {
71
+ override: t('source-node', 'Set on this holder'),
72
+ figure: t('source-builtin', 'Built-in default'),
73
+ none: t('source-none', 'Not defined (one slot, stacked)')
74
+ };
75
+ const notes = [{ text: `${t('source', 'Source')}: ${source[sel.source]}` }];
76
+ if (sel.status === 'invalid') {
77
+ notes.push({ level: 'error', text: t('invalid', 'The saved setting is invalid. Shown as one slot until fixed.') });
78
+ for (const e of sel.issues)
79
+ notes.push({ level: 'error', text: commonText(e) });
80
+ }
81
+ notes.push((mm === null || mm === void 0 ? void 0 : mm.known)
82
+ ? { text: t('unit-ok', 'Drawing unit 1 = {mm} mm', { mm: mm.mmPerUnit }) }
83
+ : { level: 'error', text: t('unit-missing', 'Drawing unit is not set (mmPerUnit). Physical placement and fit checks are unavailable.') });
84
+ return {
85
+ // 개별 설정이 없으면 실효 레이아웃(기본값)에서 편집을 시작한다
86
+ effective: value === undefined && sel.status === 'valid' ? sel.layout : undefined,
87
+ canUseDefault: value !== undefined,
88
+ presets: fmsimPresets((_c = holder.loadMount) !== null && _c !== void 0 ? _c : 'ON', t),
89
+ presetSize: (mm === null || mm === void 0 ? void 0 : mm.known) ? { width: Math.abs(state.width || 0) * mm.mmPerUnit, depth: Math.abs(state.height || 0) * mm.mmPerUnit } : {},
90
+ notes,
91
+ anchors: anchors.map(id => ({ id })),
92
+ validate: (layout) => holderLayoutIssues(layout, anchors),
93
+ issueText: issue => fmsimIssueText(issue, t)
94
+ };
95
+ }
96
+ //# sourceMappingURL=load-layout-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-layout-host.js","sourceRoot":"","sources":["../../src/editors/load-layout-host.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * fmsim 이 공통 편집기(@operato/slot-layout-editor)에 주는 것 (설계 02번 3.7.1절)\n * Tier·Slot 편집, 검증, 미리보기, 적용 전 주소 영향은 공통 편집기가 한다. 기종 프리셋·실효 레이아웃·검증은 figure-model 의\n * 것이다(ADR-0051 4.1절 결정 8). 이 패널은 도면 노드의 배치 덮어쓰기 화면이고, fmsim 은 자기 몫만 넘긴다:\n * 프리셋 이름, 홀더가 주는 Anchor, 값의 출처·도면 단위 안내, 실시간 주소 읽기 설정과 그 검증, fmsim 고유 오류 문구.\n * 화면 요소를 만들지 않는 순수 모듈이다 — 문구 함수(t)와 공통 문구 함수는 부르는 쪽이 준다.\n */\n\nimport type { SlotLayoutEditorOptions, SlotLayoutPreset } from '@operato/slot-layout-editor'\nimport {\n figureSlotLayoutIssues,\n slotLayoutPreset,\n SLOT_LAYOUT_PRESET_KEYS,\n SLOT_LAYOUT_PRESET_PARAMS,\n type LayoutIssue as SlotLayoutIssue,\n type SlotLayoutPresetKey,\n type SlotMount\n} from '@hatiolab/figure-model'\nimport { LoadAddressing, LoadLayout, validateAddressing } from '../utils/load-address'\nimport { LayoutIssue, LayoutIssueCode } from '../utils/layout-issue'\nimport { ISSUE_TEXT } from '../utils/layout-issue-text'\n\nexport type Translate = (key: string, fallback: string, options?: Record<string, unknown>) => string\n\n/** 문제 하나: 코드·값의 위치·매개변수 */\nexport type IssueLike = { code: string; path: string; params?: Record<string, string | number> }\n\n/** 공통 편집기의 문제 문구 (층·자리 검증 코드) */\nexport type CommonIssueText = (issue: IssueLike) => string\n\n// ── 기종 프리셋 (3.7절, figure-model) ─────────────────────────────────────────\n\nconst PRESET_LABELS: Record<SlotLayoutPresetKey, string> = {\n single: 'Single slot',\n agvTandem: 'AGV tandem (front/rear)',\n agvParallel: 'AGV parallel (left/right)',\n agv2x2: 'AGV 2×2',\n bufferPort2x2: 'Buffer port 2×2',\n multiBufferPort: 'Multi-buffer port (4)',\n dualTierConveyor: 'Dual-tier conveyor',\n craneTwinFork: 'Crane twin fork',\n floorBlock: 'Floor block'\n}\n\nconst PARAM_LABELS: Record<string, string> = {\n upperElevation: 'Upper tier elevation (mm)',\n rows: 'Rows',\n cols: 'Columns',\n stackLimit: 'Stack limit'\n}\n\n/** 공통 편집기의 프리셋 목록. 홀더 원점·포크 Anchor 위에 펼치고, 단일 자리는 홀더의 적재 방식으로 만든다 */\nexport function fmsimPresets(mount: SlotMount, t: Translate): SlotLayoutPreset<LoadLayout>[] {\n return SLOT_LAYOUT_PRESET_KEYS.map(key => ({\n key,\n label: t(`preset-${key}`, PRESET_LABELS[key]),\n params: SLOT_LAYOUT_PRESET_PARAMS[key].map(p => ({ key: p.key, label: t(`param-${p.key}`, PARAM_LABELS[p.key]), integer: p.integer })),\n build: (size, params) => slotLayoutPreset(key, size, params, { mount })\n }))\n}\n\n// ── 실시간 주소 읽기 (3.6절) ────────────────────────────────────────────────\n\nexport function setAddressing(layout: LoadLayout, addressing: LoadAddressing | undefined): LoadLayout {\n const next = structuredClone(layout)\n if (addressing) next.addressing = structuredClone(addressing)\n else delete next.addressing\n return next\n}\n\n// ── 편집기 옵션 ─────────────────────────────────────────────────────────────\n\n/** fmsim 고유 오류(주소 읽기 등)의 문구. fmsim 코드가 아니면 undefined — 공통 편집기 문구를 쓴다 */\nexport function fmsimIssueText(issue: IssueLike, t: Translate): string | undefined {\n const fallback = ISSUE_TEXT[issue.code as LayoutIssueCode]\n if (!fallback) return undefined\n const text = t(`issue.${issue.code}`, fallback, { ...issue.params, interpolation: { escapeValue: false } })\n return issue.path ? `${issue.path}: ${text}` : text\n}\n\n/**\n * 공통 편집기가 보는 것 밖의 검증: 실효 레이아웃이 덮어쓰기에 거는 figure 규칙(받는 타입은 생략하면 모든 아이템, 적으면\n * 비지 않은 목록, ADR-0051 4.1절 결정 5·7)과 주소 읽기 설정. 공통 검증 결과는 편집기가 이미 보이므로 figure 고유 코드만 더한다.\n */\nexport function holderLayoutIssues(layout: LoadLayout, anchors: readonly string[]): (SlotLayoutIssue | LayoutIssue)[] {\n return [...figureSlotLayoutIssues(layout, anchors).filter(i => i.code === 'ACCEPTS_EMPTY'), ...validateAddressing(layout.addressing)]\n}\n\n/** 선택한 홀더(LoadLayoutHolder)에서 공통 편집기 옵션을 만든다. value 는 도면 노드의 개별 설정 */\nexport function holderEditorOptions(holder: any, value: LoadLayout | undefined, t: Translate, commonText: CommonIssueText): SlotLayoutEditorOptions<LoadLayout> {\n const sel = holder.effectiveLayout\n const mm = holder.mmPerUnit\n const state = holder.state || {}\n const anchors: string[] = holder.slotAnchors?.() ?? []\n const source: Record<string, string> = {\n override: t('source-node', 'Set on this holder'),\n figure: t('source-builtin', 'Built-in default'),\n none: t('source-none', 'Not defined (one slot, stacked)')\n }\n const notes: SlotLayoutEditorOptions['notes'] = [{ text: `${t('source', 'Source')}: ${source[sel.source]}` }]\n if (sel.status === 'invalid') {\n notes.push({ level: 'error', text: t('invalid', 'The saved setting is invalid. Shown as one slot until fixed.') })\n for (const e of sel.issues as SlotLayoutIssue[]) notes.push({ level: 'error', text: commonText(e) })\n }\n notes.push(\n mm?.known\n ? { text: t('unit-ok', 'Drawing unit 1 = {mm} mm', { mm: mm.mmPerUnit }) }\n : { level: 'error', text: t('unit-missing', 'Drawing unit is not set (mmPerUnit). Physical placement and fit checks are unavailable.') }\n )\n return {\n // 개별 설정이 없으면 실효 레이아웃(기본값)에서 편집을 시작한다\n effective: value === undefined && sel.status === 'valid' ? sel.layout : undefined,\n canUseDefault: value !== undefined,\n presets: fmsimPresets(holder.loadMount ?? 'ON', t),\n presetSize: mm?.known ? { width: Math.abs(state.width || 0) * mm.mmPerUnit, depth: Math.abs(state.height || 0) * mm.mmPerUnit } : {},\n notes,\n anchors: anchors.map(id => ({ id })),\n validate: (layout: LoadLayout) => holderLayoutIssues(layout, anchors),\n issueText: issue => fmsimIssueText(issue, t)\n }\n}\n"]}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @license Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * "Carrier 적재 자리" 구역 (설계 02번 3.7.1절)
5
+ * 공통 편집기(@operato/slot-layout-editor 의 ox-slot-layout-editor)를 띄우고 fmsim 의 몫만 넘긴다:
6
+ * 프리셋 이름, 홀더가 주는 Anchor, 값의 출처·도면 단위, 실시간 주소 읽기 설정, fmsim 고유 오류 문구 (load-layout-host).
7
+ * 이 화면은 도면 노드의 배치 덮어쓰기다. 프리셋·실효 레이아웃·검증은 figure-model 의 것이다 (ADR-0051 4.1절 결정 8).
8
+ * [적용]이면 고친 값을, [기본값 사용]이면 개별 설정을 지운 값(undefined)을 도면 노드에 넘긴다. 실행 중 점유는 저장하지 않는다.
9
+ *
10
+ * 속성 패널(board specifics)에서 custom-editor · value-key="slotLayout" 로 쓴다. value 를 바꾸고 change 를 보낸다.
11
+ */
12
+ import { __decorate, __metadata } from "tslib";
13
+ import { css, html, nothing, LitElement } from 'lit';
14
+ import { customElement, property } from 'lit/decorators.js';
15
+ import { i18next, localize } from '@operato/i18n';
16
+ // 패키지 루트가 ox-slot-layout-editor 를 정의한다 (fmsim 의 moduleResolution 은 하위 경로 exports 를 풀지 않는다)
17
+ import { issueText as commonIssueText } from '@operato/slot-layout-editor';
18
+ import { holderEditorOptions, setAddressing } from './load-layout-host';
19
+ const t = (key, fallback, opts = {}) => i18next.t(`label.load-layout.${key}`, Object.assign({ defaultValue: fallback }, opts));
20
+ let OxLoadLayoutPanel = class OxLoadLayoutPanel extends localize(i18next)(LitElement) {
21
+ render() {
22
+ var _a;
23
+ if (!((_a = this.holder) === null || _a === void 0 ? void 0 : _a.effectiveLayout))
24
+ return nothing;
25
+ const options = Object.assign(Object.assign({}, holderEditorOptions(this.holder, this.value, t, commonIssueText)), { renderExtra: (draft, edit) => this._renderAddressing(draft, edit) });
26
+ return html `<ox-slot-layout-editor
27
+ .value=${this.value}
28
+ .options=${options}
29
+ @change=${this._apply}
30
+ @slot-layout-use-default=${this._useDefault}
31
+ ></ox-slot-layout-editor>`;
32
+ }
33
+ /** 공통 편집기의 change 는 여기서 받고, 속성 패널에는 이 요소의 change 로 다시 알린다 (value-key 가 이 요소에 있다) */
34
+ _apply(e) {
35
+ e.stopPropagation();
36
+ this.value = structuredClone(e.target.value);
37
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
38
+ }
39
+ /** 기본값 사용: 개별 설정을 지운다. 값을 비우는 편집과 다르다 */
40
+ _useDefault(e) {
41
+ e.stopPropagation();
42
+ this.value = undefined;
43
+ this.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
44
+ }
45
+ /** 실시간 데이터에서 주소를 읽는 규칙 (3.6절). 공통 편집기의 renderExtra 자리에 그린다 */
46
+ _renderAddressing(draft, edit) {
47
+ const a = draft.addressing;
48
+ const set = (next) => edit(setAddressing(draft, next));
49
+ const text = (field, label, value, onChange, placeholder = '') => html `
50
+ <div class="row">
51
+ <label>${label}</label>
52
+ <input
53
+ data-field=${field}
54
+ .value=${value !== null && value !== void 0 ? value : ''}
55
+ placeholder=${placeholder}
56
+ @change=${(e) => onChange(e.target.value.trim())}
57
+ />
58
+ </div>
59
+ `;
60
+ return html `
61
+ <h4>${t('addressing', 'Address in live data')}</h4>
62
+ <div class="row">
63
+ <label>${t('addressing-by', 'Read by')}</label>
64
+ <select
65
+ data-field="addressing-by"
66
+ @change=${(e) => {
67
+ const by = e.target.value;
68
+ set(by === 'FIELDS'
69
+ ? { by, tierField: 'TIER', slotField: 'SLOT' }
70
+ : by === 'PATTERN'
71
+ ? { by, field: 'CARRIERLOC', pattern: '{slot}' }
72
+ : undefined);
73
+ }}
74
+ >
75
+ <option value="" ?selected=${!a}>${t('addressing-none', 'Not read (all shown as address unknown)')}</option>
76
+ <option value="FIELDS" ?selected=${(a === null || a === void 0 ? void 0 : a.by) === 'FIELDS'}>${t('addressing-fields', 'Fields')}</option>
77
+ <option value="PATTERN" ?selected=${(a === null || a === void 0 ? void 0 : a.by) === 'PATTERN'}>${t('addressing-pattern', 'Location pattern')}</option>
78
+ </select>
79
+ </div>
80
+ ${(a === null || a === void 0 ? void 0 : a.by) === 'FIELDS'
81
+ ? html `${text('tier-field', t('tier-field', 'Tier field'), a.tierField, v => set(Object.assign(Object.assign({}, a), { tierField: v || undefined })), t('tier-field-empty', 'Empty = tier 1'))}
82
+ ${text('slot-field', t('slot-field', 'Slot field'), a.slotField, v => set(Object.assign(Object.assign({}, a), { slotField: v })))}`
83
+ : nothing}
84
+ ${(a === null || a === void 0 ? void 0 : a.by) === 'PATTERN'
85
+ ? html `${text('field', t('field', 'Field'), a.field, v => set(Object.assign(Object.assign({}, a), { field: v })))}
86
+ ${text('pattern', t('pattern-text', 'Pattern'), a.pattern, v => set(Object.assign(Object.assign({}, a), { pattern: v })), '{rack}-{slot:02}-{tier:02}')}`
87
+ : nothing}
88
+ `;
89
+ }
90
+ };
91
+ OxLoadLayoutPanel.styles = css `
92
+ :host {
93
+ display: block;
94
+ font-size: 12px;
95
+ }
96
+ h4 {
97
+ margin: 6px 0 2px;
98
+ font-size: 12px;
99
+ }
100
+ .row {
101
+ display: grid;
102
+ grid-template-columns: 40% 1fr;
103
+ align-items: center;
104
+ gap: 4px;
105
+ }
106
+ input,
107
+ select {
108
+ font-size: 12px;
109
+ min-width: 0;
110
+ }
111
+ `;
112
+ __decorate([
113
+ property({ attribute: false }),
114
+ __metadata("design:type", Object)
115
+ ], OxLoadLayoutPanel.prototype, "holder", void 0);
116
+ __decorate([
117
+ property({ attribute: false }),
118
+ __metadata("design:type", Object)
119
+ ], OxLoadLayoutPanel.prototype, "value", void 0);
120
+ OxLoadLayoutPanel = __decorate([
121
+ customElement('ox-load-layout-panel')
122
+ ], OxLoadLayoutPanel);
123
+ export { OxLoadLayoutPanel };
124
+ //# sourceMappingURL=ox-load-layout-panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ox-load-layout-panel.js","sourceRoot":"","sources":["../../src/editors/ox-load-layout-panel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @license Copyright © HatioLab Inc. All rights reserved.\n *\n * \"Carrier 적재 자리\" 구역 (설계 02번 3.7.1절)\n * 공통 편집기(@operato/slot-layout-editor 의 ox-slot-layout-editor)를 띄우고 fmsim 의 몫만 넘긴다:\n * 프리셋 이름, 홀더가 주는 Anchor, 값의 출처·도면 단위, 실시간 주소 읽기 설정, fmsim 고유 오류 문구 (load-layout-host).\n * 이 화면은 도면 노드의 배치 덮어쓰기다. 프리셋·실효 레이아웃·검증은 figure-model 의 것이다 (ADR-0051 4.1절 결정 8).\n * [적용]이면 고친 값을, [기본값 사용]이면 개별 설정을 지운 값(undefined)을 도면 노드에 넘긴다. 실행 중 점유는 저장하지 않는다.\n *\n * 속성 패널(board specifics)에서 custom-editor · value-key=\"slotLayout\" 로 쓴다. value 를 바꾸고 change 를 보낸다.\n */\n\nimport { css, html, nothing, LitElement } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\nimport { i18next, localize } from '@operato/i18n'\n// 패키지 루트가 ox-slot-layout-editor 를 정의한다 (fmsim 의 moduleResolution 은 하위 경로 exports 를 풀지 않는다)\nimport { issueText as commonIssueText } from '@operato/slot-layout-editor'\n\nimport { LoadAddressing, LoadLayout } from '../utils/load-address'\nimport { holderEditorOptions, setAddressing } from './load-layout-host'\n\nconst t = (key: string, fallback: string, opts: Record<string, unknown> = {}) =>\n i18next.t(`label.load-layout.${key}`, { defaultValue: fallback, ...opts })\n\n@customElement('ox-load-layout-panel')\nexport class OxLoadLayoutPanel extends localize(i18next)(LitElement) {\n static styles = css`\n :host {\n display: block;\n font-size: 12px;\n }\n h4 {\n margin: 6px 0 2px;\n font-size: 12px;\n }\n .row {\n display: grid;\n grid-template-columns: 40% 1fr;\n align-items: center;\n gap: 4px;\n }\n input,\n select {\n font-size: 12px;\n min-width: 0;\n }\n `\n\n /** 선택한 홀더 (LoadLayoutHolder) */\n @property({ attribute: false }) holder: any\n /** 도면 노드의 개별 설정 (state.slotLayout) */\n @property({ attribute: false }) value: LoadLayout | undefined\n\n render() {\n if (!this.holder?.effectiveLayout) return nothing\n const options = {\n ...holderEditorOptions(this.holder, this.value, t, commonIssueText),\n renderExtra: (draft: LoadLayout, edit: (next: LoadLayout) => void) => this._renderAddressing(draft, edit)\n }\n return html`<ox-slot-layout-editor\n .value=${this.value}\n .options=${options}\n @change=${this._apply}\n @slot-layout-use-default=${this._useDefault}\n ></ox-slot-layout-editor>`\n }\n\n /** 공통 편집기의 change 는 여기서 받고, 속성 패널에는 이 요소의 change 로 다시 알린다 (value-key 가 이 요소에 있다) */\n private _apply(e: Event) {\n e.stopPropagation()\n this.value = structuredClone((e.target as any).value)\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n\n /** 기본값 사용: 개별 설정을 지운다. 값을 비우는 편집과 다르다 */\n private _useDefault(e: Event) {\n e.stopPropagation()\n this.value = undefined\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n\n /** 실시간 데이터에서 주소를 읽는 규칙 (3.6절). 공통 편집기의 renderExtra 자리에 그린다 */\n private _renderAddressing(draft: LoadLayout, edit: (next: LoadLayout) => void) {\n const a = draft.addressing\n const set = (next: LoadAddressing | undefined) => edit(setAddressing(draft, next))\n const text = (field: string, label: string, value: string | undefined, onChange: (v: string) => void, placeholder = '') => html`\n <div class=\"row\">\n <label>${label}</label>\n <input\n data-field=${field}\n .value=${value ?? ''}\n placeholder=${placeholder}\n @change=${(e: Event) => onChange((e.target as HTMLInputElement).value.trim())}\n />\n </div>\n `\n return html`\n <h4>${t('addressing', 'Address in live data')}</h4>\n <div class=\"row\">\n <label>${t('addressing-by', 'Read by')}</label>\n <select\n data-field=\"addressing-by\"\n @change=${(e: Event) => {\n const by = (e.target as HTMLSelectElement).value\n set(\n by === 'FIELDS'\n ? { by, tierField: 'TIER', slotField: 'SLOT' }\n : by === 'PATTERN'\n ? { by, field: 'CARRIERLOC', pattern: '{slot}' }\n : undefined\n )\n }}\n >\n <option value=\"\" ?selected=${!a}>${t('addressing-none', 'Not read (all shown as address unknown)')}</option>\n <option value=\"FIELDS\" ?selected=${a?.by === 'FIELDS'}>${t('addressing-fields', 'Fields')}</option>\n <option value=\"PATTERN\" ?selected=${a?.by === 'PATTERN'}>${t('addressing-pattern', 'Location pattern')}</option>\n </select>\n </div>\n ${a?.by === 'FIELDS'\n ? html`${text('tier-field', t('tier-field', 'Tier field'), a.tierField, v => set({ ...a, tierField: v || undefined }), t('tier-field-empty', 'Empty = tier 1'))}\n ${text('slot-field', t('slot-field', 'Slot field'), a.slotField, v => set({ ...a, slotField: v }))}`\n : nothing}\n ${a?.by === 'PATTERN'\n ? html`${text('field', t('field', 'Field'), a.field, v => set({ ...a, field: v }))}\n ${text('pattern', t('pattern-text', 'Pattern'), a.pattern, v => set({ ...a, pattern: v }), '{rack}-{slot:02}-{tier:02}')}`\n : nothing}\n `\n }\n}\n"]}
@@ -14,6 +14,8 @@ import * as THREE from 'three';
14
14
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
15
15
  import { RealObjectGroup, registerRealObjectFactory, MaterialBank } from '@hatiolab/things-scene';
16
16
  import { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js';
17
+ import { craneForkCount } from '../utils/crane-forks';
18
+ import { forkAnchorId } from '@hatiolab/figure-model';
17
19
  const CLAMP_COLOR = 0x444444;
18
20
  const FORK_COLOR = 0x999999;
19
21
  const DEFAULT_BODY_COLOR = 0xcc8844;
@@ -22,6 +24,14 @@ export class Crane3D extends RealObjectGroup {
22
24
  super(...arguments);
23
25
  this.forkMastBaseY = 0;
24
26
  this.forkMastH = 0;
27
+ /** 포크별 Anchor 노드 (fork_1, fork_2). 캐리지(forkGroup) 아래에서 각자 뻗는다 */
28
+ this.forkNodes = new Map();
29
+ /** 포크 판 윗면 높이 (포크 노드 로컬). Carrier 는 이 면에 놓인다 */
30
+ this.forkSurfaceY = 0;
31
+ /** 포크가 끝까지(100) 뻗었을 때의 거리 (+Z, 도면 y 방향) */
32
+ this.forkReach = 0;
33
+ /** 포크 Anchor 노드: 포크 판 윗면의 중심. 포크 노드 아래에 있어 포크가 오르내리고 뻗으면 함께 간다 */
34
+ this.forkAnchors = new Map();
25
35
  }
26
36
  get effectiveDepth() {
27
37
  const { depth } = this.component.state;
@@ -38,6 +48,32 @@ export class Crane3D extends RealObjectGroup {
38
48
  const clamped = Math.max(0, Math.min(100, Number(z) || 0));
39
49
  this.forkGroup.position.y = this.forkMastBaseY + (clamped / 100) * this.forkMastH;
40
50
  }
51
+ /** 포크 n 을 0~100 만큼 뻗는다 (다른 포크는 그대로) */
52
+ applyForkExtend(fork, extend) {
53
+ const node = this.forkNodes.get(forkAnchorId(fork));
54
+ if (!node)
55
+ return;
56
+ const clamped = Math.max(0, Math.min(100, Number(extend) || 0));
57
+ node.position.z = (clamped / 100) * this.forkReach;
58
+ }
59
+ /**
60
+ * A fork's anchor (common item contract 3.4): a node at the centre of its plate's top, under the fork, so what it
61
+ * holds goes up, down and out with it. Null when the crane has no such fork.
62
+ */
63
+ forkAnchor(anchorId) {
64
+ const fork = this.forkNodes.get(anchorId);
65
+ if (!fork)
66
+ return null;
67
+ let node = this.forkAnchors.get(anchorId);
68
+ if (!node || node.parent !== fork) {
69
+ node = new THREE.Group();
70
+ node.name = `${anchorId}-anchor`;
71
+ fork.add(node);
72
+ this.forkAnchors.set(anchorId, node);
73
+ }
74
+ node.position.set(0, this.forkSurfaceY, 0);
75
+ return node;
76
+ }
41
77
  resolveColor() {
42
78
  const comp = this.component;
43
79
  try {
@@ -111,38 +147,57 @@ export class Crane3D extends RealObjectGroup {
111
147
  // 일 때 mast 바닥 자리에. forkZ 0~100 에 따라 group.position.y 만 변경하면
112
148
  // mast 따라 위/아래 이동. mesh 재구축 불필요.
113
149
  const forkMat = MaterialBank.material('crane|fork', { roughness: 0.4, metalness: 0.5 }, FORK_COLOR);
114
- const forkGeometries = [];
115
- const plateW = w * 0.9;
150
+ const forkCount = craneForkCount(this.component.state.forkCount);
116
151
  const plateH = d * 0.08;
117
152
  const plateD = pillarThick * 0.8;
118
- const centerPlate = new THREE.BoxGeometry(plateW, plateH, plateD);
119
- centerPlate.translate(0, plateH / 2, 0); // local 0 = mast 바닥
120
- forkGeometries.push(centerPlate);
121
153
  const armW = pillarThick * 0.6;
122
154
  const armH = plateH * 0.4;
123
155
  const armD = h * 0.6;
124
- const armSpacing = w * 0.5;
125
- for (const zSign of [-1, 1]) {
126
- for (const xSign of [-1, 1]) {
127
- const arm = new THREE.BoxGeometry(armW, armH, armD);
128
- arm.translate(xSign * (armSpacing / 2), armH / 2, zSign * (plateD / 2 + armD / 2));
129
- forkGeometries.push(arm);
156
+ /* 포크 하나: 가로 판 + 판 앞뒤로 뻗은 팔 두 쌍. 로컬 0 = 판 바닥, 중심 x = 0 */
157
+ const buildFork = (plateW) => {
158
+ const parts = [];
159
+ const plate = new THREE.BoxGeometry(plateW, plateH, plateD);
160
+ plate.translate(0, plateH / 2, 0);
161
+ parts.push(plate);
162
+ const armSpacing = plateW * 0.55;
163
+ for (const zSign of [-1, 1]) {
164
+ for (const xSign of [-1, 1]) {
165
+ const arm = new THREE.BoxGeometry(armW, armH, armD);
166
+ arm.translate(xSign * (armSpacing / 2), armH / 2, zSign * (plateD / 2 + armD / 2));
167
+ parts.push(arm);
168
+ }
130
169
  }
131
- }
132
- const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat);
133
- forkMesh.castShadow = true;
170
+ const mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(parts), forkMat);
171
+ mesh.castShadow = true;
172
+ return mesh;
173
+ };
174
+ // 캐리지: forkZ 로 오르내린다. 포크는 캐리지 안에서 각자 Z 로 뻗는다 (트윈 포크의 독립 이동)
134
175
  const forkGroup = new THREE.Group();
135
- forkGroup.add(forkMesh);
136
176
  inner.add(forkGroup);
177
+ this.forkNodes.clear();
178
+ const forkW = forkCount === 2 ? w * 0.42 : w * 0.9;
179
+ for (let i = 1; i <= forkCount; i++) {
180
+ const node = new THREE.Group();
181
+ node.name = forkAnchorId(i);
182
+ node.position.x = forkCount === 2 ? (i === 1 ? -1 : 1) * w * 0.24 : 0;
183
+ node.add(buildFork(forkW));
184
+ forkGroup.add(node);
185
+ this.forkNodes.set(node.name, node);
186
+ }
137
187
  this.forkGroup = forkGroup;
138
188
  this.forkMastBaseY = mastBaseY;
189
+ this.forkSurfaceY = plateH;
190
+ this.forkReach = h * 0.8;
139
191
  // forkZ=100 시점에 mast 의 *_위쪽 한계*. 포크 자체 두께 (plateH) 만큼은 마스트
140
192
  // 안에 머무르도록 limit 보정 — 그렇지 않으면 forkZ=100 일 때 포크가 상단
141
193
  // 클램프와 충돌. mastH * 0.92 로 약간 보수적 (= 마스트 본체 92% 까지만 이동).
142
194
  this.forkMastH = mastH * 0.92;
143
- // 초기 forkZ 반영 — state.forkZ 가 이미 set 되어 있으면 그 위치에 배치
144
- const initialForkZ = Number(this.component.state.forkZ);
195
+ // 초기 forkZ·포크 뻗음 반영 — state 에 이미 있으면 그 위치에 배치
196
+ const state = this.component.state;
197
+ const initialForkZ = Number(state.forkZ);
145
198
  this.applyForkZ(Number.isFinite(initialForkZ) ? initialForkZ : 0);
199
+ for (let i = 1; i <= forkCount; i++)
200
+ this.applyForkExtend(i, Number(state[`forkExtend${i}`]) || 0);
146
201
  // ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──
147
202
  const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD);
148
203
  const topClamp = new THREE.Mesh(topClampGeo, clampMat);
@@ -157,13 +212,17 @@ export class Crane3D extends RealObjectGroup {
157
212
  /** build() 직전 호출. forkGroup 참조를 깨끗이 비워둔다 — 새 build 가 다시 set. */
158
213
  clear() {
159
214
  this.forkGroup = undefined;
215
+ this.forkNodes.clear();
160
216
  super.clear();
161
217
  }
162
218
  onchange(after, before) {
163
- if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {
219
+ if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after || 'forkCount' in after) {
164
220
  this.update();
165
221
  return;
166
222
  }
223
+ for (const fork of [1, 2])
224
+ if (`forkExtend${fork}` in after)
225
+ this.applyForkExtend(fork, Number(after[`forkExtend${fork}`]) || 0);
167
226
  // forkZ 는 mesh 재구축 없이 group.position.y 만 갱신. 같은 onchange 에서
168
227
  // left/top 도 함께 변경될 수 있으므로 super 도 반드시 호출해 본체 transform
169
228
  // 갱신 path (state.left/top → obj3d.position.x/z) 가 동작하게 함.
@@ -1 +1 @@
1
- {"version":3,"file":"crane-3d.js","sourceRoot":"","sources":["../../src/factories/crane-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Crane (Stacker Crane) 3D Factory\n *\n * 스토커 내부의 스태커 크레인 — CraneRail 위를 이동하는 차체.\n * 레일 인프라는 CraneRail이 담당하므로 크레인은 이동체만 표현.\n *\n * 구조 (바닥 원점, y=0~depth):\n * - 하단 바퀴/클램프 (4%, 바닥 레일에 물리는 부분)\n * - 수직 마스트 (좌우 기둥 88%, statusColor 적용)\n * - 캐리지 포크 (마스트 40% 높이, 양쪽 포크 암)\n * - 상단 클램프 (4%, 천장 레일에 물리는 부분)\n */\n\nimport * as THREE from 'three'\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'\nimport { RealObjectGroup, registerRealObjectFactory, MaterialBank } from '@hatiolab/things-scene'\nimport type { Component } from '@hatiolab/things-scene'\nimport { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'\n\nconst CLAMP_COLOR = 0x444444\nconst FORK_COLOR = 0x999999\nconst DEFAULT_BODY_COLOR = 0xcc8844\n\nexport class Crane3D extends RealObjectGroup {\n /** 캐리지 포크 group — Crane.state.forkZ (0~100) 가 변하면 position.y 만 lerp.\n * build() 시 inner 에 add 되고, *_mastBaseY 와 mastH 를 함께 보관* 해서\n * setForkZ 가 비율 계산에 사용. */\n private forkGroup?: THREE.Group\n private forkMastBaseY = 0\n private forkMastH = 0\n\n get effectiveDepth(): number {\n const { depth } = this.component.state\n return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2\n }\n\n /**\n * 0~100 정규화 z (= state.forkZ) 를 mast 높이 비율로 변환해서 포크 group 의\n * three-up y 위치 갱신. build() 가 안 끝났으면 noop — build 가 forkZ 를 다시\n * 반영하면서 끝난다.\n */\n applyForkZ(z: number): void {\n if (!this.forkGroup) return\n const clamped = Math.max(0, Math.min(100, Number(z) || 0))\n this.forkGroup.position.y = this.forkMastBaseY + (clamped / 100) * this.forkMastH\n }\n\n private resolveColor(): number {\n const comp = this.component as any\n\n try {\n const sc = comp.statusColor\n if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()\n } catch {\n /* statusColor 미구현 시 무시 */\n }\n\n const { fillStyle } = this.component.state\n if (fillStyle && typeof fillStyle === 'string') {\n try {\n return new THREE.Color(fillStyle).getHex()\n } catch {\n /* 파싱 실패 시 무시 */\n }\n }\n\n return DEFAULT_BODY_COLOR\n }\n\n build() {\n super.build()\n\n const { width = 10, height = 10 } = this.component.state\n const w = Math.abs(width)\n const h = Math.abs(height)\n const d = this.effectiveDepth\n if (!w || !h) return\n\n const inner = this.components3D\n\n // 높이 배분: 하단 클램프 4%, 마스트 92%, 상단 클램프 4%\n const clampH = d * 0.04\n const mastH = d * 0.92\n\n const pillarThick = Math.min(w, h) * 0.12\n\n // MaterialBank 공유(dedup). 자체 update() 없이 base 생명주기(guarded disposeObject3D) 상속 → 안전.\n const clampMat = MaterialBank.material('crane|clamp', { roughness: 0.5, metalness: 0.5 }, CLAMP_COLOR)\n\n // ── 1. 하단 클램프 (바닥 레일에 물리는 바퀴부) ──\n const clampW = w * 0.7\n const clampD = h * 0.5\n const bottomClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)\n const bottomClamp = new THREE.Mesh(bottomClampGeo, clampMat)\n bottomClamp.position.y = clampH / 2\n bottomClamp.castShadow = true\n inner.add(bottomClamp)\n\n // ── 2. 수직 마스트 (좌우 기둥 + 상·하 가로 연결 바) ──\n const mastBaseY = clampH\n const bodyColor = this.resolveColor()\n const mastMat = MaterialBank.material('crane|mast', { roughness: 0.3, metalness: 0.6 }, bodyColor)\n\n const mastGeometries: THREE.BufferGeometry[] = []\n\n // 좌우 수직 기둥\n for (const xSign of [-1, 1]) {\n const pillar = new THREE.BoxGeometry(pillarThick, mastH, pillarThick)\n pillar.translate(xSign * (w / 2 - pillarThick / 2), mastBaseY + mastH / 2, 0)\n mastGeometries.push(pillar)\n }\n\n // 하단 가로 연결 바\n const crossW = w - pillarThick\n const crossH = pillarThick * 0.5\n const lowerCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)\n lowerCross.translate(0, mastBaseY + crossH / 2, 0)\n mastGeometries.push(lowerCross)\n\n // 상단 가로 연결 바\n const upperCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)\n upperCross.translate(0, mastBaseY + mastH - crossH / 2, 0)\n mastGeometries.push(upperCross)\n\n const mastMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(mastGeometries), mastMat)\n mastMesh.castShadow = true\n inner.add(mastMesh)\n\n // ── 3. 캐리지 포크 (별도 group — state.forkZ 변경 시 group.position.y lerp) ──\n // 포크 geometry 는 *_mastBaseY 기준 local 0 에 둠* — 즉 group.position.y = 0\n // 일 때 mast 바닥 자리에. forkZ 0~100 에 따라 group.position.y 만 변경하면\n // mast 따라 위/아래 이동. mesh 재구축 불필요.\n const forkMat = MaterialBank.material('crane|fork', { roughness: 0.4, metalness: 0.5 }, FORK_COLOR)\n\n const forkGeometries: THREE.BufferGeometry[] = []\n\n const plateW = w * 0.9\n const plateH = d * 0.08\n const plateD = pillarThick * 0.8\n const centerPlate = new THREE.BoxGeometry(plateW, plateH, plateD)\n centerPlate.translate(0, plateH / 2, 0) // local 0 = mast 바닥\n forkGeometries.push(centerPlate)\n\n const armW = pillarThick * 0.6\n const armH = plateH * 0.4\n const armD = h * 0.6\n const armSpacing = w * 0.5\n\n for (const zSign of [-1, 1]) {\n for (const xSign of [-1, 1]) {\n const arm = new THREE.BoxGeometry(armW, armH, armD)\n arm.translate(xSign * (armSpacing / 2), armH / 2, zSign * (plateD / 2 + armD / 2))\n forkGeometries.push(arm)\n }\n }\n\n const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat)\n forkMesh.castShadow = true\n\n const forkGroup = new THREE.Group()\n forkGroup.add(forkMesh)\n inner.add(forkGroup)\n this.forkGroup = forkGroup\n this.forkMastBaseY = mastBaseY\n // forkZ=100 시점에 mast 의 *_위쪽 한계*. 포크 자체 두께 (plateH) 만큼은 마스트\n // 안에 머무르도록 limit 보정 — 그렇지 않으면 forkZ=100 일 때 포크가 상단\n // 클램프와 충돌. mastH * 0.92 로 약간 보수적 (= 마스트 본체 92% 까지만 이동).\n this.forkMastH = mastH * 0.92\n\n // 초기 forkZ 반영 — state.forkZ 가 이미 set 되어 있으면 그 위치에 배치\n const initialForkZ = Number((this.component.state as any).forkZ)\n this.applyForkZ(Number.isFinite(initialForkZ) ? initialForkZ : 0)\n\n // ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──\n const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)\n const topClamp = new THREE.Mesh(topClampGeo, clampMat)\n topClamp.position.y = mastBaseY + mastH + clampH / 2\n topClamp.castShadow = true\n inner.add(topClamp)\n\n // mesh 는 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).\n this.updateAlpha()\n }\n\n updateDimension() {}\n // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴(faint).\n\n /** build() 직전 호출. forkGroup 참조를 깨끗이 비워둔다 — 새 build 가 다시 set. */\n clear() {\n this.forkGroup = undefined\n super.clear()\n }\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {\n this.update()\n return\n }\n // forkZ 는 mesh 재구축 없이 group.position.y 만 갱신. 같은 onchange 에서\n // left/top 도 함께 변경될 수 있으므로 super 도 반드시 호출해 본체 transform\n // 갱신 path (state.left/top → obj3d.position.x/z) 가 동작하게 함.\n if ('forkZ' in after) {\n this.applyForkZ(Number(after.forkZ) || 0)\n }\n super.onchange(after, before)\n }\n}\n\nregisterRealObjectFactory('Crane', (component: Component) => new Crane3D(component))\n"]}
1
+ {"version":3,"file":"crane-3d.js","sourceRoot":"","sources":["../../src/factories/crane-3d.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Crane (Stacker Crane) 3D Factory\n *\n * 스토커 내부의 스태커 크레인 — CraneRail 위를 이동하는 차체.\n * 레일 인프라는 CraneRail이 담당하므로 크레인은 이동체만 표현.\n *\n * 구조 (바닥 원점, y=0~depth):\n * - 하단 바퀴/클램프 (4%, 바닥 레일에 물리는 부분)\n * - 수직 마스트 (좌우 기둥 88%, statusColor 적용)\n * - 캐리지 포크 (마스트 40% 높이, 양쪽 포크 암)\n * - 상단 클램프 (4%, 천장 레일에 물리는 부분)\n */\n\nimport * as THREE from 'three'\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'\nimport { RealObjectGroup, registerRealObjectFactory, MaterialBank } from '@hatiolab/things-scene'\nimport type { Component } from '@hatiolab/things-scene'\nimport { TRANSPORT_SURFACE_HEIGHT } from './machine-3d.js'\nimport { craneForkCount } from '../utils/crane-forks'\nimport { forkAnchorId } from '@hatiolab/figure-model'\n\nconst CLAMP_COLOR = 0x444444\nconst FORK_COLOR = 0x999999\nconst DEFAULT_BODY_COLOR = 0xcc8844\n\nexport class Crane3D extends RealObjectGroup {\n /** 캐리지 포크 group — Crane.state.forkZ (0~100) 가 변하면 position.y 만 lerp.\n * build() 시 inner 에 add 되고, *_mastBaseY 와 mastH 를 함께 보관* 해서\n * setForkZ 가 비율 계산에 사용. */\n private forkGroup?: THREE.Group\n private forkMastBaseY = 0\n private forkMastH = 0\n /** 포크별 Anchor 노드 (fork_1, fork_2). 캐리지(forkGroup) 아래에서 각자 뻗는다 */\n private forkNodes = new Map<string, THREE.Group>()\n /** 포크 판 윗면 높이 (포크 노드 로컬). Carrier 는 이 면에 놓인다 */\n private forkSurfaceY = 0\n /** 포크가 끝까지(100) 뻗었을 때의 거리 (+Z, 도면 y 방향) */\n private forkReach = 0\n\n get effectiveDepth(): number {\n const { depth } = this.component.state\n return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2\n }\n\n /**\n * 0~100 정규화 z (= state.forkZ) 를 mast 높이 비율로 변환해서 포크 group 의\n * three-up y 위치 갱신. build() 가 안 끝났으면 noop — build 가 forkZ 를 다시\n * 반영하면서 끝난다.\n */\n applyForkZ(z: number): void {\n if (!this.forkGroup) return\n const clamped = Math.max(0, Math.min(100, Number(z) || 0))\n this.forkGroup.position.y = this.forkMastBaseY + (clamped / 100) * this.forkMastH\n }\n\n /** 포크 n 을 0~100 만큼 뻗는다 (다른 포크는 그대로) */\n applyForkExtend(fork: number, extend: number): void {\n const node = this.forkNodes.get(forkAnchorId(fork))\n if (!node) return\n const clamped = Math.max(0, Math.min(100, Number(extend) || 0))\n node.position.z = (clamped / 100) * this.forkReach\n }\n\n /** 포크 Anchor 노드: 포크 판 윗면의 중심. 포크 노드 아래에 있어 포크가 오르내리고 뻗으면 함께 간다 */\n private forkAnchors = new Map<string, THREE.Group>()\n\n /**\n * A fork's anchor (common item contract 3.4): a node at the centre of its plate's top, under the fork, so what it\n * holds goes up, down and out with it. Null when the crane has no such fork.\n */\n forkAnchor(anchorId: string): THREE.Object3D | null {\n const fork = this.forkNodes.get(anchorId)\n if (!fork) return null\n let node = this.forkAnchors.get(anchorId)\n if (!node || node.parent !== fork) {\n node = new THREE.Group()\n node.name = `${anchorId}-anchor`\n fork.add(node)\n this.forkAnchors.set(anchorId, node)\n }\n node.position.set(0, this.forkSurfaceY, 0)\n return node\n }\n\n private resolveColor(): number {\n const comp = this.component as any\n\n try {\n const sc = comp.statusColor\n if (typeof sc === 'string' && sc && sc.toUpperCase() !== '#F0F0F0') return new THREE.Color(sc).getHex()\n } catch {\n /* statusColor 미구현 시 무시 */\n }\n\n const { fillStyle } = this.component.state\n if (fillStyle && typeof fillStyle === 'string') {\n try {\n return new THREE.Color(fillStyle).getHex()\n } catch {\n /* 파싱 실패 시 무시 */\n }\n }\n\n return DEFAULT_BODY_COLOR\n }\n\n build() {\n super.build()\n\n const { width = 10, height = 10 } = this.component.state\n const w = Math.abs(width)\n const h = Math.abs(height)\n const d = this.effectiveDepth\n if (!w || !h) return\n\n const inner = this.components3D\n\n // 높이 배분: 하단 클램프 4%, 마스트 92%, 상단 클램프 4%\n const clampH = d * 0.04\n const mastH = d * 0.92\n\n const pillarThick = Math.min(w, h) * 0.12\n\n // MaterialBank 공유(dedup). 자체 update() 없이 base 생명주기(guarded disposeObject3D) 상속 → 안전.\n const clampMat = MaterialBank.material('crane|clamp', { roughness: 0.5, metalness: 0.5 }, CLAMP_COLOR)\n\n // ── 1. 하단 클램프 (바닥 레일에 물리는 바퀴부) ──\n const clampW = w * 0.7\n const clampD = h * 0.5\n const bottomClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)\n const bottomClamp = new THREE.Mesh(bottomClampGeo, clampMat)\n bottomClamp.position.y = clampH / 2\n bottomClamp.castShadow = true\n inner.add(bottomClamp)\n\n // ── 2. 수직 마스트 (좌우 기둥 + 상·하 가로 연결 바) ──\n const mastBaseY = clampH\n const bodyColor = this.resolveColor()\n const mastMat = MaterialBank.material('crane|mast', { roughness: 0.3, metalness: 0.6 }, bodyColor)\n\n const mastGeometries: THREE.BufferGeometry[] = []\n\n // 좌우 수직 기둥\n for (const xSign of [-1, 1]) {\n const pillar = new THREE.BoxGeometry(pillarThick, mastH, pillarThick)\n pillar.translate(xSign * (w / 2 - pillarThick / 2), mastBaseY + mastH / 2, 0)\n mastGeometries.push(pillar)\n }\n\n // 하단 가로 연결 바\n const crossW = w - pillarThick\n const crossH = pillarThick * 0.5\n const lowerCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)\n lowerCross.translate(0, mastBaseY + crossH / 2, 0)\n mastGeometries.push(lowerCross)\n\n // 상단 가로 연결 바\n const upperCross = new THREE.BoxGeometry(crossW, crossH, pillarThick)\n upperCross.translate(0, mastBaseY + mastH - crossH / 2, 0)\n mastGeometries.push(upperCross)\n\n const mastMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(mastGeometries), mastMat)\n mastMesh.castShadow = true\n inner.add(mastMesh)\n\n // ── 3. 캐리지 포크 (별도 group — state.forkZ 변경 시 group.position.y lerp) ──\n // 포크 geometry 는 *_mastBaseY 기준 local 0 에 둠* — 즉 group.position.y = 0\n // 일 때 mast 바닥 자리에. forkZ 0~100 에 따라 group.position.y 만 변경하면\n // mast 따라 위/아래 이동. mesh 재구축 불필요.\n const forkMat = MaterialBank.material('crane|fork', { roughness: 0.4, metalness: 0.5 }, FORK_COLOR)\n const forkCount = craneForkCount((this.component.state as any).forkCount)\n\n const plateH = d * 0.08\n const plateD = pillarThick * 0.8\n const armW = pillarThick * 0.6\n const armH = plateH * 0.4\n const armD = h * 0.6\n\n /* 포크 하나: 가로 판 + 판 앞뒤로 뻗은 팔 두 쌍. 로컬 0 = 판 바닥, 중심 x = 0 */\n const buildFork = (plateW: number) => {\n const parts: THREE.BufferGeometry[] = []\n const plate = new THREE.BoxGeometry(plateW, plateH, plateD)\n plate.translate(0, plateH / 2, 0)\n parts.push(plate)\n const armSpacing = plateW * 0.55\n for (const zSign of [-1, 1]) {\n for (const xSign of [-1, 1]) {\n const arm = new THREE.BoxGeometry(armW, armH, armD)\n arm.translate(xSign * (armSpacing / 2), armH / 2, zSign * (plateD / 2 + armD / 2))\n parts.push(arm)\n }\n }\n const mesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(parts), forkMat)\n mesh.castShadow = true\n return mesh\n }\n\n // 캐리지: forkZ 로 오르내린다. 포크는 캐리지 안에서 각자 Z 로 뻗는다 (트윈 포크의 독립 이동)\n const forkGroup = new THREE.Group()\n inner.add(forkGroup)\n this.forkNodes.clear()\n const forkW = forkCount === 2 ? w * 0.42 : w * 0.9\n for (let i = 1; i <= forkCount; i++) {\n const node = new THREE.Group()\n node.name = forkAnchorId(i)\n node.position.x = forkCount === 2 ? (i === 1 ? -1 : 1) * w * 0.24 : 0\n node.add(buildFork(forkW))\n forkGroup.add(node)\n this.forkNodes.set(node.name, node)\n }\n this.forkGroup = forkGroup\n this.forkMastBaseY = mastBaseY\n this.forkSurfaceY = plateH\n this.forkReach = h * 0.8\n // forkZ=100 시점에 mast 의 *_위쪽 한계*. 포크 자체 두께 (plateH) 만큼은 마스트\n // 안에 머무르도록 limit 보정 — 그렇지 않으면 forkZ=100 일 때 포크가 상단\n // 클램프와 충돌. mastH * 0.92 로 약간 보수적 (= 마스트 본체 92% 까지만 이동).\n this.forkMastH = mastH * 0.92\n\n // 초기 forkZ·포크 뻗음 반영 — state 에 이미 있으면 그 위치에 배치\n const state = this.component.state as any\n const initialForkZ = Number(state.forkZ)\n this.applyForkZ(Number.isFinite(initialForkZ) ? initialForkZ : 0)\n for (let i = 1; i <= forkCount; i++) this.applyForkExtend(i, Number(state[`forkExtend${i}`]) || 0)\n\n // ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──\n const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD)\n const topClamp = new THREE.Mesh(topClampGeo, clampMat)\n topClamp.position.y = mastBaseY + mastH + clampH / 2\n topClamp.castShadow = true\n inner.add(topClamp)\n\n // mesh 는 super.build() 이후 생성 → 코어 faint 를 명시 적용(편집 흐림이 mesh 에 닿게).\n this.updateAlpha()\n }\n\n updateDimension() {}\n // updateAlpha override 제거 — 코어 RealObject.updateAlpha(=applyMaterialOpacity)로 수렴(faint).\n\n /** build() 직전 호출. forkGroup 참조를 깨끗이 비워둔다 — 새 build 가 다시 set. */\n clear() {\n this.forkGroup = undefined\n this.forkNodes.clear()\n super.clear()\n }\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after || 'forkCount' in after) {\n this.update()\n return\n }\n for (const fork of [1, 2]) if (`forkExtend${fork}` in after) this.applyForkExtend(fork, Number(after[`forkExtend${fork}`]) || 0)\n // forkZ 는 mesh 재구축 없이 group.position.y 만 갱신. 같은 onchange 에서\n // left/top 도 함께 변경될 수 있으므로 super 도 반드시 호출해 본체 transform\n // 갱신 path (state.left/top → obj3d.position.x/z) 가 동작하게 함.\n if ('forkZ' in after) {\n this.applyForkZ(Number(after.forkZ) || 0)\n }\n super.onchange(after, before)\n }\n}\n\nregisterRealObjectFactory('Crane', (component: Component) => new Crane3D(component))\n"]}