@fmsim/machine 2.0.0-beta.86 → 2.0.0-beta.87

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/dist/crane.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { __decorate } from "tslib";
2
- import { sceneComponent } from '@hatiolab/things-scene';
2
+ import { sceneComponent, frameClock } from '@hatiolab/things-scene';
3
3
  import MCSVehicle from './mcs-vehicle';
4
4
  const NATURE = {
5
5
  mutable: false,
@@ -8,6 +8,17 @@ const NATURE = {
8
8
  properties: [...MCSVehicle.properties]
9
9
  };
10
10
  const GAP = 2;
11
+ /** 좌표 보간 기본 duration (ms). data.DURATION_MS 가 없으면 이 값 사용. */
12
+ const DEFAULT_DURATION_MS = 1000;
13
+ function clamp01(v) {
14
+ return Math.max(0, Math.min(100, Number(v) || 0));
15
+ }
16
+ function lerp(a, b, t) {
17
+ return a + (b - a) * t;
18
+ }
19
+ function easeInOut(t) {
20
+ return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
21
+ }
11
22
  let Crane = class Crane extends MCSVehicle {
12
23
  static get nature() {
13
24
  return NATURE;
@@ -18,6 +29,128 @@ let Crane = class Crane extends MCSVehicle {
18
29
  getLegendFallback() {
19
30
  return this.root.craneLegendTheme || super.getLegendFallback();
20
31
  }
32
+ /**
33
+ * data.POSITION = { X, Y, Z } (각 0~100 정규화) 분기. Stocker 부모 기준으로
34
+ * 실 좌표 (state.left/top) 와 forkZ 로 변환해 lerp 시작. 보간 중 새 값이
35
+ * 들어오면 *현재 보간 위치* 를 from 으로 잡고 새 target 으로 새 lerp 시작.
36
+ * NODEID 경로는 super (MCSVehicle.onchangeData) 가 처리 — POSITION 만 있는
37
+ * 데이터에선 NODEID 가 없어 자연스럽게 noop.
38
+ */
39
+ onchangeData(after, before) {
40
+ const afterData = after === null || after === void 0 ? void 0 : after.data;
41
+ const beforeData = before === null || before === void 0 ? void 0 : before.data;
42
+ const afterPos = afterData === null || afterData === void 0 ? void 0 : afterData.POSITION;
43
+ if (afterPos && typeof afterPos === 'object') {
44
+ const beforePos = beforeData === null || beforeData === void 0 ? void 0 : beforeData.POSITION;
45
+ const changed = afterPos.X !== (beforePos === null || beforePos === void 0 ? void 0 : beforePos.X) ||
46
+ afterPos.Y !== (beforePos === null || beforePos === void 0 ? void 0 : beforePos.Y) ||
47
+ afterPos.Z !== (beforePos === null || beforePos === void 0 ? void 0 : beforePos.Z);
48
+ if (changed) {
49
+ this._startPositionAnimation(afterPos, afterData === null || afterData === void 0 ? void 0 : afterData.DURATION_MS);
50
+ }
51
+ }
52
+ super.onchangeData(after, before);
53
+ }
54
+ dispose() {
55
+ if (this._posAnimator) {
56
+ this._posAnimator.unsubscribe();
57
+ this._posAnimator = undefined;
58
+ }
59
+ super.dispose();
60
+ }
61
+ _startPositionAnimation(pos, durationMs) {
62
+ const target = this._normalizedToReal(pos);
63
+ if (!target)
64
+ return; // 부모가 Stocker 아니면 변환 skip
65
+ const now = frameClock.simTime;
66
+ const duration = Math.max(1, Number(durationMs) || DEFAULT_DURATION_MS);
67
+ // 현재 위치 — 보간 중이면 그 시점 보간값을 from 으로
68
+ let fromLeft = Number(this.state.left) || 0;
69
+ let fromTop = Number(this.state.top) || 0;
70
+ let fromForkZ = Number(this.state.forkZ) || 0;
71
+ if (this._posAnimator) {
72
+ const a = this._posAnimator;
73
+ const t = Math.min(1, Math.max(0, (now - a.startTime) / a.duration));
74
+ const e = easeInOut(t);
75
+ fromLeft = lerp(a.fromLeft, a.toLeft, e);
76
+ fromTop = lerp(a.fromTop, a.toTop, e);
77
+ fromForkZ = lerp(a.fromForkZ, a.toForkZ, e);
78
+ a.unsubscribe();
79
+ this._posAnimator = undefined;
80
+ }
81
+ const unsubscribe = frameClock.subscribe((_dt, simTime) => this._tickPositionAnimation(simTime), { description: 'crane-position', component: this });
82
+ this._posAnimator = {
83
+ fromLeft,
84
+ fromTop,
85
+ fromForkZ,
86
+ toLeft: target.left,
87
+ toTop: target.top,
88
+ toForkZ: target.forkZ,
89
+ startTime: now,
90
+ duration,
91
+ unsubscribe
92
+ };
93
+ // 즉시 첫 frame 반영 (t=0) — set 한 번 호출. paused 환경에서도 from→target
94
+ // 진입 시점이 명확.
95
+ this._tickPositionAnimation(now);
96
+ }
97
+ _tickPositionAnimation(simTime) {
98
+ const a = this._posAnimator;
99
+ if (!a)
100
+ return;
101
+ const t = Math.min(1, Math.max(0, (simTime - a.startTime) / a.duration));
102
+ const e = easeInOut(t);
103
+ this.set({
104
+ left: lerp(a.fromLeft, a.toLeft, e),
105
+ top: lerp(a.fromTop, a.toTop, e),
106
+ forkZ: lerp(a.fromForkZ, a.toForkZ, e)
107
+ });
108
+ if (t >= 1) {
109
+ a.unsubscribe();
110
+ this._posAnimator = undefined;
111
+ }
112
+ }
113
+ /**
114
+ * 0~100 정규화 좌표 → state.left/top + forkZ.
115
+ *
116
+ * 부모 오브젝트 (보통은 Stocker 이지만 다른 컨테이너일 수도) 의 width/height
117
+ * 만 사용해 좌표 매핑 — 타입 무관 일반화. 가상 rail 은 host 의 긴쪽 중심부
118
+ * 가정:
119
+ * - 데이터.X → host 의 긴쪽 (자동 판별: max(width, height))
120
+ * - 데이터.Y → 짧은쪽 (rail 중심 = 50 근처가 일반적)
121
+ * - 데이터.Z → forkZ (state 에 0~100 그대로 저장, 3D 측에서 mast 비율로 매핑)
122
+ *
123
+ * 참조는 ParentObjectMixin 이 제공하는 `parentObject` 사용 —
124
+ * state.parentObjectId 명시 시 그것, 미명시 시 this.parent 자동 fallback.
125
+ */
126
+ _normalizedToReal(pos) {
127
+ var _a, _b;
128
+ const host = this.parentObject;
129
+ if (!host)
130
+ return undefined;
131
+ const hw = Number((_a = host.state) === null || _a === void 0 ? void 0 : _a.width) || 0;
132
+ const hh = Number((_b = host.state) === null || _b === void 0 ? void 0 : _b.height) || 0;
133
+ const cw = Number(this.state.width) || 0;
134
+ const ch = Number(this.state.height) || 0;
135
+ if (!hw || !hh)
136
+ return undefined;
137
+ const longIsWidth = hw >= hh;
138
+ const longLen = longIsWidth ? hw : hh;
139
+ const shortLen = longIsWidth ? hh : hw;
140
+ const craneLongSize = longIsWidth ? cw : ch;
141
+ const craneShortSize = longIsWidth ? ch : cw;
142
+ const X = clamp01(pos.X);
143
+ const Y = clamp01(pos.Y);
144
+ // crane 의 *center* 가 (X/100)*longLen, (Y/100)*shortLen 에 오도록.
145
+ // state.left/top 은 top-left 기준이므로 size/2 차감.
146
+ const longPos = (X / 100) * longLen - craneLongSize / 2;
147
+ const shortPos = (Y / 100) * shortLen - craneShortSize / 2;
148
+ return {
149
+ left: longIsWidth ? longPos : shortPos,
150
+ top: longIsWidth ? shortPos : longPos,
151
+ forkZ: clamp01(pos.Z)
152
+ };
153
+ }
21
154
  render(ctx) {
22
155
  var { left, top, width, height } = this.bounds;
23
156
  const { x: cx, y: cy } = this.center;
package/dist/crane.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"crane.js","sourceRoot":"","sources":["../src/crane.ts"],"names":[],"mappings":"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","sourcesContent":["import { Component, ComponentNature, sceneComponent } from '@hatiolab/things-scene'\nimport MCSVehicle from './mcs-vehicle'\nimport { LEGEND_CRANE } from './features/mcs-status-default'\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [...MCSVehicle.properties]\n}\n\nconst GAP = 2\n\n@sceneComponent('Crane')\nexport default class Crane extends MCSVehicle {\n static get nature() {\n return NATURE\n }\n\n get auxColor() {\n return 'black'\n }\n\n getLegendFallback() {\n return (this.root as any).craneLegendTheme || super.getLegendFallback()\n }\n\n render(ctx: CanvasRenderingContext2D) {\n var { left, top, width, height } = this.bounds\n const { x: cx, y: cy } = this.center\n const rx = width / 2\n const ry = height / 2\n\n const round = Math.min(rx, ry) * 0.5\n\n ctx.translate(left, top)\n ctx.beginPath()\n\n ctx.strokeStyle = this.auxColor\n ctx.fillStyle = this.statusColor!\n ctx.lineWidth = 1\n ctx.lineCap = 'round'\n ctx.setLineDash([])\n\n const radius = Math.round(Math.min(width, height) * 0.1)\n ctx.roundRect(1, 0, width - 2, height, radius)\n ctx.roundRect(GAP + 1, GAP, width - (GAP + 1) * 2, height - GAP * 2, radius - 1)\n ctx.fill()\n ctx.stroke()\n\n ctx.beginPath()\n ctx.arc(cx - left, cy - top, round, 0, Math.PI * 2)\n ctx.stroke()\n\n ctx.beginPath()\n ctx.roundRect(width / 2 - (round * 2) / 4 - round / 8, height / 2 - (round * 6) / 5, round / 4, (round * 6) / 5, 1)\n ctx.roundRect(width / 2 + (round * 2) / 4 - round / 8, height / 2 - (round * 6) / 5, round / 4, (round * 6) / 5, 1)\n\n ctx.fill()\n ctx.stroke()\n\n ctx.beginPath()\n\n // draw crane bar metapore\n ctx.strokeStyle = 'black'\n ctx.lineWidth = 3\n ctx.lineCap = 'butt'\n\n ctx.moveTo(GAP * 3, height / 2)\n ctx.lineTo(width - GAP * 3, height / 2)\n\n ctx.stroke()\n\n ctx.translate(-left, -top)\n }\n}\n"]}
1
+ {"version":3,"file":"crane.js","sourceRoot":"","sources":["../src/crane.ts"],"names":[],"mappings":"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","sourcesContent":["import { Component, ComponentNature, sceneComponent, frameClock } from '@hatiolab/things-scene'\nimport MCSVehicle from './mcs-vehicle'\nimport { LEGEND_CRANE } from './features/mcs-status-default'\n\nconst NATURE: ComponentNature = {\n mutable: false,\n resizable: true,\n rotatable: true,\n properties: [...MCSVehicle.properties]\n}\n\nconst GAP = 2\n\n/** 좌표 보간 기본 duration (ms). data.DURATION_MS 가 없으면 이 값 사용. */\nconst DEFAULT_DURATION_MS = 1000\n\nfunction clamp01(v: number): number {\n return Math.max(0, Math.min(100, Number(v) || 0))\n}\n\nfunction lerp(a: number, b: number, t: number): number {\n return a + (b - a) * t\n}\n\nfunction easeInOut(t: number): number {\n return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2\n}\n\ninterface CranePositionAnimator {\n fromLeft: number\n fromTop: number\n fromForkZ: number\n toLeft: number\n toTop: number\n toForkZ: number\n startTime: number\n duration: number\n unsubscribe: () => void\n}\n\n@sceneComponent('Crane')\nexport default class Crane extends MCSVehicle {\n static get nature() {\n return NATURE\n }\n\n get auxColor() {\n return 'black'\n }\n\n getLegendFallback() {\n return (this.root as any).craneLegendTheme || super.getLegendFallback()\n }\n\n private _posAnimator?: CranePositionAnimator\n\n /**\n * data.POSITION = { X, Y, Z } (각 0~100 정규화) 분기. Stocker 부모 기준으로\n * 실 좌표 (state.left/top) 와 forkZ 로 변환해 lerp 시작. 보간 중 새 값이\n * 들어오면 *현재 보간 위치* 를 from 으로 잡고 새 target 으로 새 lerp 시작.\n * NODEID 경로는 super (MCSVehicle.onchangeData) 가 처리 — POSITION 만 있는\n * 데이터에선 NODEID 가 없어 자연스럽게 noop.\n */\n onchangeData(after: Record<string, any>, before: Record<string, any>): void {\n const afterData = after?.data\n const beforeData = before?.data\n const afterPos = afterData?.POSITION\n if (afterPos && typeof afterPos === 'object') {\n const beforePos = beforeData?.POSITION\n const changed =\n afterPos.X !== beforePos?.X ||\n afterPos.Y !== beforePos?.Y ||\n afterPos.Z !== beforePos?.Z\n if (changed) {\n this._startPositionAnimation(afterPos, afterData?.DURATION_MS)\n }\n }\n super.onchangeData(after, before)\n }\n\n dispose() {\n if (this._posAnimator) {\n this._posAnimator.unsubscribe()\n this._posAnimator = undefined\n }\n super.dispose()\n }\n\n private _startPositionAnimation(\n pos: { X: number; Y: number; Z: number },\n durationMs?: number\n ): void {\n const target = this._normalizedToReal(pos)\n if (!target) return // 부모가 Stocker 아니면 변환 skip\n\n const now = frameClock.simTime\n const duration = Math.max(1, Number(durationMs) || DEFAULT_DURATION_MS)\n\n // 현재 위치 — 보간 중이면 그 시점 보간값을 from 으로\n let fromLeft = Number(this.state.left) || 0\n let fromTop = Number(this.state.top) || 0\n let fromForkZ = Number((this.state as any).forkZ) || 0\n\n if (this._posAnimator) {\n const a = this._posAnimator\n const t = Math.min(1, Math.max(0, (now - a.startTime) / a.duration))\n const e = easeInOut(t)\n fromLeft = lerp(a.fromLeft, a.toLeft, e)\n fromTop = lerp(a.fromTop, a.toTop, e)\n fromForkZ = lerp(a.fromForkZ, a.toForkZ, e)\n a.unsubscribe()\n this._posAnimator = undefined\n }\n\n const unsubscribe = frameClock.subscribe(\n (_dt: number, simTime: number) => this._tickPositionAnimation(simTime),\n { description: 'crane-position', component: this }\n )\n\n this._posAnimator = {\n fromLeft,\n fromTop,\n fromForkZ,\n toLeft: target.left,\n toTop: target.top,\n toForkZ: target.forkZ,\n startTime: now,\n duration,\n unsubscribe\n }\n // 즉시 첫 frame 반영 (t=0) — set 한 번 호출. paused 환경에서도 from→target\n // 진입 시점이 명확.\n this._tickPositionAnimation(now)\n }\n\n private _tickPositionAnimation(simTime: number): void {\n const a = this._posAnimator\n if (!a) return\n const t = Math.min(1, Math.max(0, (simTime - a.startTime) / a.duration))\n const e = easeInOut(t)\n this.set({\n left: lerp(a.fromLeft, a.toLeft, e),\n top: lerp(a.fromTop, a.toTop, e),\n forkZ: lerp(a.fromForkZ, a.toForkZ, e)\n })\n if (t >= 1) {\n a.unsubscribe()\n this._posAnimator = undefined\n }\n }\n\n /**\n * 0~100 정규화 좌표 → state.left/top + forkZ.\n *\n * 부모 오브젝트 (보통은 Stocker 이지만 다른 컨테이너일 수도) 의 width/height\n * 만 사용해 좌표 매핑 — 타입 무관 일반화. 가상 rail 은 host 의 긴쪽 중심부\n * 가정:\n * - 데이터.X → host 의 긴쪽 (자동 판별: max(width, height))\n * - 데이터.Y → 짧은쪽 (rail 중심 = 50 근처가 일반적)\n * - 데이터.Z → forkZ (state 에 0~100 그대로 저장, 3D 측에서 mast 비율로 매핑)\n *\n * 참조는 ParentObjectMixin 이 제공하는 `parentObject` 사용 —\n * state.parentObjectId 명시 시 그것, 미명시 시 this.parent 자동 fallback.\n */\n private _normalizedToReal(\n pos: { X: number; Y: number; Z: number }\n ): { left: number; top: number; forkZ: number } | undefined {\n const host: any = (this as any).parentObject\n if (!host) return undefined\n\n const hw = Number(host.state?.width) || 0\n const hh = Number(host.state?.height) || 0\n const cw = Number(this.state.width) || 0\n const ch = Number(this.state.height) || 0\n if (!hw || !hh) return undefined\n\n const longIsWidth = hw >= hh\n const longLen = longIsWidth ? hw : hh\n const shortLen = longIsWidth ? hh : hw\n const craneLongSize = longIsWidth ? cw : ch\n const craneShortSize = longIsWidth ? ch : cw\n\n const X = clamp01(pos.X)\n const Y = clamp01(pos.Y)\n\n // crane 의 *center* 가 (X/100)*longLen, (Y/100)*shortLen 에 오도록.\n // state.left/top 은 top-left 기준이므로 size/2 차감.\n const longPos = (X / 100) * longLen - craneLongSize / 2\n const shortPos = (Y / 100) * shortLen - craneShortSize / 2\n\n return {\n left: longIsWidth ? longPos : shortPos,\n top: longIsWidth ? shortPos : longPos,\n forkZ: clamp01(pos.Z)\n }\n }\n\n render(ctx: CanvasRenderingContext2D) {\n var { left, top, width, height } = this.bounds\n const { x: cx, y: cy } = this.center\n const rx = width / 2\n const ry = height / 2\n\n const round = Math.min(rx, ry) * 0.5\n\n ctx.translate(left, top)\n ctx.beginPath()\n\n ctx.strokeStyle = this.auxColor\n ctx.fillStyle = this.statusColor!\n ctx.lineWidth = 1\n ctx.lineCap = 'round'\n ctx.setLineDash([])\n\n const radius = Math.round(Math.min(width, height) * 0.1)\n ctx.roundRect(1, 0, width - 2, height, radius)\n ctx.roundRect(GAP + 1, GAP, width - (GAP + 1) * 2, height - GAP * 2, radius - 1)\n ctx.fill()\n ctx.stroke()\n\n ctx.beginPath()\n ctx.arc(cx - left, cy - top, round, 0, Math.PI * 2)\n ctx.stroke()\n\n ctx.beginPath()\n ctx.roundRect(width / 2 - (round * 2) / 4 - round / 8, height / 2 - (round * 6) / 5, round / 4, (round * 6) / 5, 1)\n ctx.roundRect(width / 2 + (round * 2) / 4 - round / 8, height / 2 - (round * 6) / 5, round / 4, (round * 6) / 5, 1)\n\n ctx.fill()\n ctx.stroke()\n\n ctx.beginPath()\n\n // draw crane bar metapore\n ctx.strokeStyle = 'black'\n ctx.lineWidth = 3\n ctx.lineCap = 'butt'\n\n ctx.moveTo(GAP * 3, height / 2)\n ctx.lineTo(width - GAP * 3, height / 2)\n\n ctx.stroke()\n\n ctx.translate(-left, -top)\n }\n}\n"]}
@@ -18,10 +18,26 @@ const CLAMP_COLOR = 0x444444;
18
18
  const FORK_COLOR = 0x999999;
19
19
  const DEFAULT_BODY_COLOR = 0xcc8844;
20
20
  export class Crane3D extends RealObjectGroup {
21
+ constructor() {
22
+ super(...arguments);
23
+ this.forkMastBaseY = 0;
24
+ this.forkMastH = 0;
25
+ }
21
26
  get effectiveDepth() {
22
27
  const { depth } = this.component.state;
23
28
  return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2;
24
29
  }
30
+ /**
31
+ * 0~100 정규화 z (= state.forkZ) 를 mast 높이 비율로 변환해서 포크 group 의
32
+ * three-up y 위치 갱신. build() 가 안 끝났으면 noop — build 가 forkZ 를 다시
33
+ * 반영하면서 끝난다.
34
+ */
35
+ applyForkZ(z) {
36
+ if (!this.forkGroup)
37
+ return;
38
+ const clamped = Math.max(0, Math.min(100, Number(z) || 0));
39
+ this.forkGroup.position.y = this.forkMastBaseY + (clamped / 100) * this.forkMastH;
40
+ }
25
41
  resolveColor() {
26
42
  const comp = this.component;
27
43
  try {
@@ -97,22 +113,22 @@ export class Crane3D extends RealObjectGroup {
97
113
  const mastMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(mastGeometries), mastMat);
98
114
  mastMesh.castShadow = true;
99
115
  inner.add(mastMesh);
100
- // ── 3. 캐리지 포크 (마스트 40% 높이, 양쪽으로 뻗는 포크 암) ──
101
- const forkY = mastBaseY + mastH * 0.4;
116
+ // ── 3. 캐리지 포크 (별도 group — state.forkZ 변경 시 group.position.y lerp) ──
117
+ // 포크 geometry 는 *_mastBaseY 기준 local 0 에 둠* — 즉 group.position.y = 0
118
+ // 일 때 mast 바닥 자리에. forkZ 0~100 에 따라 group.position.y 만 변경하면
119
+ // mast 따라 위/아래 이동. mesh 재구축 불필요.
102
120
  const forkMat = new THREE.MeshStandardMaterial({
103
121
  color: FORK_COLOR,
104
122
  roughness: 0.4,
105
123
  metalness: 0.5,
106
124
  });
107
125
  const forkGeometries = [];
108
- // 캐리지 중심 플레이트 (마스트에 부착, 양쪽 포크의 베이스)
109
126
  const plateW = w * 0.9;
110
127
  const plateH = d * 0.08;
111
128
  const plateD = pillarThick * 0.8;
112
129
  const centerPlate = new THREE.BoxGeometry(plateW, plateH, plateD);
113
- centerPlate.translate(0, forkY + plateH / 2, 0);
130
+ centerPlate.translate(0, plateH / 2, 0); // local 0 = mast 바닥
114
131
  forkGeometries.push(centerPlate);
115
- // 양쪽 포크 암 (좌/우 선반 방향으로 뻗음)
116
132
  const armW = pillarThick * 0.6;
117
133
  const armH = plateH * 0.4;
118
134
  const armD = h * 0.6;
@@ -120,13 +136,24 @@ export class Crane3D extends RealObjectGroup {
120
136
  for (const zSign of [-1, 1]) {
121
137
  for (const xSign of [-1, 1]) {
122
138
  const arm = new THREE.BoxGeometry(armW, armH, armD);
123
- arm.translate(xSign * (armSpacing / 2), forkY + armH / 2, zSign * (plateD / 2 + armD / 2));
139
+ arm.translate(xSign * (armSpacing / 2), armH / 2, zSign * (plateD / 2 + armD / 2));
124
140
  forkGeometries.push(arm);
125
141
  }
126
142
  }
127
143
  const forkMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(forkGeometries), forkMat);
128
144
  forkMesh.castShadow = true;
129
- inner.add(forkMesh);
145
+ const forkGroup = new THREE.Group();
146
+ forkGroup.add(forkMesh);
147
+ inner.add(forkGroup);
148
+ this.forkGroup = forkGroup;
149
+ this.forkMastBaseY = mastBaseY;
150
+ // forkZ=100 시점에 mast 의 *_위쪽 한계*. 포크 자체 두께 (plateH) 만큼은 마스트
151
+ // 안에 머무르도록 limit 보정 — 그렇지 않으면 forkZ=100 일 때 포크가 상단
152
+ // 클램프와 충돌. mastH * 0.92 로 약간 보수적 (= 마스트 본체 92% 까지만 이동).
153
+ this.forkMastH = mastH * 0.92;
154
+ // 초기 forkZ 반영 — state.forkZ 가 이미 set 되어 있으면 그 위치에 배치
155
+ const initialForkZ = Number(this.component.state.forkZ);
156
+ this.applyForkZ(Number.isFinite(initialForkZ) ? initialForkZ : 0);
130
157
  // ── 4. 상단 클램프 (천장 레일에 물리는 가이드부) ──
131
158
  const topClampGeo = new THREE.BoxGeometry(clampW, clampH, clampD);
132
159
  const topClamp = new THREE.Mesh(topClampGeo, clampMat);
@@ -136,11 +163,22 @@ export class Crane3D extends RealObjectGroup {
136
163
  }
137
164
  updateDimension() { }
138
165
  updateAlpha() { }
166
+ /** build() 직전 호출. forkGroup 참조를 깨끗이 비워둔다 — 새 build 가 다시 set. */
167
+ clear() {
168
+ this.forkGroup = undefined;
169
+ super.clear();
170
+ }
139
171
  onchange(after, before) {
140
172
  if ('width' in after || 'height' in after || 'depth' in after || 'fillStyle' in after || 'strokeStyle' in after) {
141
173
  this.update();
142
174
  return;
143
175
  }
176
+ // forkZ 는 mesh 재구축 없이 group.position.y 만 갱신. 같은 onchange 에서
177
+ // left/top 도 함께 변경될 수 있으므로 super 도 반드시 호출해 본체 transform
178
+ // 갱신 path (state.left/top → obj3d.position.x/z) 가 동작하게 함.
179
+ if ('forkZ' in after) {
180
+ this.applyForkZ(Number(after.forkZ) || 0);
181
+ }
144
182
  super.onchange(after, before);
145
183
  }
146
184
  }
@@ -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 } 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 get effectiveDepth(): number {\n const { depth } = this.component.state\n return depth || 5 * TRANSPORT_SURFACE_HEIGHT - 2\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 const clampMat = new THREE.MeshStandardMaterial({\n color: CLAMP_COLOR,\n roughness: 0.5,\n metalness: 0.5,\n })\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 = new THREE.MeshStandardMaterial({\n color: bodyColor,\n roughness: 0.3,\n metalness: 0.6,\n })\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. 캐리지 포크 (마스트 40% 높이, 양쪽으로 뻗는 포크 암) ──\n const forkY = mastBaseY + mastH * 0.4\n const forkMat = new THREE.MeshStandardMaterial({\n color: FORK_COLOR,\n roughness: 0.4,\n metalness: 0.5,\n })\n\n const forkGeometries: THREE.BufferGeometry[] = []\n\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, forkY + plateH / 2, 0)\n forkGeometries.push(centerPlate)\n\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), forkY + 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 inner.add(forkMesh)\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\n updateDimension() {}\n updateAlpha() {}\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 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 } 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 const clampMat = new THREE.MeshStandardMaterial({\n color: CLAMP_COLOR,\n roughness: 0.5,\n metalness: 0.5,\n })\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 = new THREE.MeshStandardMaterial({\n color: bodyColor,\n roughness: 0.3,\n metalness: 0.6,\n })\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 = new THREE.MeshStandardMaterial({\n color: FORK_COLOR,\n roughness: 0.4,\n metalness: 0.5,\n })\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\n updateDimension() {}\n updateAlpha() {}\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"]}