@operato/scene-wheel-sorter 9.1.1 → 10.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.
- package/dist/conveyor-3d.d.ts +22 -0
- package/dist/conveyor-3d.js +257 -0
- package/dist/conveyor-3d.js.map +1 -0
- package/dist/conveyor-join-3d.d.ts +19 -0
- package/dist/conveyor-join-3d.js +288 -0
- package/dist/conveyor-join-3d.js.map +1 -0
- package/dist/conveyor-join-trapezoid-3d.d.ts +19 -0
- package/dist/conveyor-join-trapezoid-3d.js +243 -0
- package/dist/conveyor-join-trapezoid-3d.js.map +1 -0
- package/dist/conveyor-join-trapezoid.d.ts +2 -1
- package/dist/conveyor-join-trapezoid.js +10 -1
- package/dist/conveyor-join-trapezoid.js.map +1 -1
- package/dist/conveyor-join.d.ts +2 -1
- package/dist/conveyor-join.js +10 -1
- package/dist/conveyor-join.js.map +1 -1
- package/dist/conveyor.d.ts +2 -1
- package/dist/conveyor.js +10 -1
- package/dist/conveyor.js.map +1 -1
- package/dist/mixin-conveyor.js +1 -1
- package/dist/mixin-conveyor.js.map +1 -1
- package/dist/scanner-3d.d.ts +13 -0
- package/dist/scanner-3d.js +146 -0
- package/dist/scanner-3d.js.map +1 -0
- package/dist/scanner.d.ts +2 -1
- package/dist/scanner.js +10 -1
- package/dist/scanner.js.map +1 -1
- package/dist/templates/index.js +3 -3
- package/dist/templates/index.js.map +1 -1
- package/dist/wheel-sorter-3d.d.ts +14 -0
- package/dist/wheel-sorter-3d.js +189 -0
- package/dist/wheel-sorter-3d.js.map +1 -0
- package/dist/wheel-sorter.d.ts +2 -1
- package/dist/wheel-sorter.js +10 -1
- package/dist/wheel-sorter.js.map +1 -1
- package/package.json +15 -13
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { RealObjectGroup } from '@hatiolab/things-scene';
|
|
2
|
+
export declare class Conveyor3D extends RealObjectGroup {
|
|
3
|
+
get effectiveDepth(): number;
|
|
4
|
+
get position(): {
|
|
5
|
+
x: number;
|
|
6
|
+
y: any;
|
|
7
|
+
z: number;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* syncFromObject3D 역변환 시 zPos 오프셋.
|
|
11
|
+
* position.y = zPos + effectiveDepth/2 이므로, 역변환도 effectiveDepth/2를 빼야 한다.
|
|
12
|
+
*/
|
|
13
|
+
protected get syncZPosOffset(): number;
|
|
14
|
+
build(): void;
|
|
15
|
+
private buildRollers;
|
|
16
|
+
private buildBelt;
|
|
17
|
+
/** Control box (motor/controller housing) + status indicator light */
|
|
18
|
+
private buildControlBox;
|
|
19
|
+
updateDimension(): void;
|
|
20
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>): void;
|
|
21
|
+
updateAlpha(): void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Straight Conveyor 3D Model
|
|
5
|
+
*
|
|
6
|
+
* Roller type: side rails + evenly-spaced cylindrical rollers + 4 legs with cross-bracing
|
|
7
|
+
* Belt type: side rails + two end drums with belt wrapping over them + 4 legs with cross-bracing
|
|
8
|
+
* Both types include a control box at one end and a status indicator light.
|
|
9
|
+
*/
|
|
10
|
+
import * as THREE from 'three';
|
|
11
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
12
|
+
import { RealObjectGroup } from '@hatiolab/things-scene';
|
|
13
|
+
const FILL_COLORS = {
|
|
14
|
+
0: 0xcccccc, // IDLE
|
|
15
|
+
1: 0xafd0f1, // RUN
|
|
16
|
+
2: 0xafd0f1, // REVERSE
|
|
17
|
+
3: 0xffba00, // WARN
|
|
18
|
+
4: 0xe9746b // ERROR
|
|
19
|
+
};
|
|
20
|
+
const FRAME_COLOR = 0x888899;
|
|
21
|
+
const ROLLER_COLOR = 0xaaaabc;
|
|
22
|
+
const CONTROL_BOX_COLOR = 0x556677;
|
|
23
|
+
const STATUS_EMISSIVE = {
|
|
24
|
+
0: 0x333333,
|
|
25
|
+
1: 0x44aaff,
|
|
26
|
+
2: 0x44aaff,
|
|
27
|
+
3: 0xffaa00,
|
|
28
|
+
4: 0xff3333
|
|
29
|
+
};
|
|
30
|
+
export class Conveyor3D extends RealObjectGroup {
|
|
31
|
+
get effectiveDepth() {
|
|
32
|
+
const { width, height, depth } = this.component.state;
|
|
33
|
+
return depth || Math.min(width, height) * 1.5;
|
|
34
|
+
}
|
|
35
|
+
get position() {
|
|
36
|
+
const { zPos = 0 } = this.component.state;
|
|
37
|
+
return {
|
|
38
|
+
x: this.cx,
|
|
39
|
+
y: zPos + this.effectiveDepth / 2,
|
|
40
|
+
z: this.cy
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* syncFromObject3D 역변환 시 zPos 오프셋.
|
|
45
|
+
* position.y = zPos + effectiveDepth/2 이므로, 역변환도 effectiveDepth/2를 빼야 한다.
|
|
46
|
+
*/
|
|
47
|
+
get syncZPosOffset() {
|
|
48
|
+
return this.effectiveDepth / 2;
|
|
49
|
+
}
|
|
50
|
+
build() {
|
|
51
|
+
super.build();
|
|
52
|
+
const { width, height, conveyorType = 0, value = 0, rollWidth = 10 } = this.component.state;
|
|
53
|
+
const depth = this.effectiveDepth;
|
|
54
|
+
// 프레임 두께: rollWidth(롤러 직경)와 동일 단위. 롤러를 감쌀 수 있어야 한다.
|
|
55
|
+
const rollerDiameter = Math.max(rollWidth, 2);
|
|
56
|
+
const frameH = rollerDiameter;
|
|
57
|
+
const legH = Math.max(depth - frameH, 0);
|
|
58
|
+
const railW = Math.max(height * 0.06, 2);
|
|
59
|
+
const legThickness = Math.max(railW * 0.8, 2);
|
|
60
|
+
const frameMaterial = new THREE.MeshStandardMaterial({
|
|
61
|
+
color: FRAME_COLOR,
|
|
62
|
+
metalness: 0.85,
|
|
63
|
+
roughness: 0.35
|
|
64
|
+
});
|
|
65
|
+
// --- Frame: side rails + legs + cross-bracing ---
|
|
66
|
+
const frameGeometries = [];
|
|
67
|
+
const railY = depth / 2 - frameH / 2;
|
|
68
|
+
// Side rails (along X)
|
|
69
|
+
for (const zSign of [-1, 1]) {
|
|
70
|
+
const rail = new THREE.BoxGeometry(width, frameH, railW);
|
|
71
|
+
rail.translate(0, railY, zSign * (height / 2 - railW / 2));
|
|
72
|
+
frameGeometries.push(rail);
|
|
73
|
+
}
|
|
74
|
+
// 4 legs at corners — 레일 바로 아래에 정렬
|
|
75
|
+
const legTopY = depth / 2 - frameH;
|
|
76
|
+
const legCenterY = legTopY - legH / 2;
|
|
77
|
+
for (const xSign of [-1, 1]) {
|
|
78
|
+
for (const zSign of [-1, 1]) {
|
|
79
|
+
const lx = xSign * (width / 2 - legThickness / 2);
|
|
80
|
+
const lz = zSign * (height / 2 - railW / 2);
|
|
81
|
+
const leg = new THREE.BoxGeometry(legThickness, legH, legThickness);
|
|
82
|
+
leg.translate(lx, legCenterY, lz);
|
|
83
|
+
frameGeometries.push(leg);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Cross-bracing: 다리 사이 연결
|
|
87
|
+
const braceH = legThickness * 0.6;
|
|
88
|
+
const braceW = legThickness * 0.6;
|
|
89
|
+
const braceLen = height - railW;
|
|
90
|
+
const braceY = legTopY - legH * 0.35;
|
|
91
|
+
for (const xSign of [-1, 1]) {
|
|
92
|
+
const brace = new THREE.BoxGeometry(braceW, braceH, braceLen);
|
|
93
|
+
brace.translate(xSign * (width / 2 - legThickness / 2), braceY, 0);
|
|
94
|
+
frameGeometries.push(brace);
|
|
95
|
+
}
|
|
96
|
+
// Cross-bracing: 좌우 다리 연결
|
|
97
|
+
const braceLenX = width - legThickness;
|
|
98
|
+
for (const zSign of [-1, 1]) {
|
|
99
|
+
const brace = new THREE.BoxGeometry(braceLenX, braceH, braceW);
|
|
100
|
+
brace.translate(0, braceY, zSign * (height / 2 - railW / 2));
|
|
101
|
+
frameGeometries.push(brace);
|
|
102
|
+
}
|
|
103
|
+
const frameMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(frameGeometries), frameMaterial);
|
|
104
|
+
frameMesh.castShadow = true;
|
|
105
|
+
frameMesh.receiveShadow = true;
|
|
106
|
+
this.object3d.add(frameMesh);
|
|
107
|
+
// --- Rollers or Belt ---
|
|
108
|
+
if (conveyorType === 1) {
|
|
109
|
+
this.buildBelt(width, height, depth, frameH, railW, value);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
this.buildRollers(width, height, depth, frameH, railW, rollWidth);
|
|
113
|
+
}
|
|
114
|
+
// --- Control box (motor housing at one end) ---
|
|
115
|
+
this.buildControlBox(width, height, depth, frameH, railW, value);
|
|
116
|
+
}
|
|
117
|
+
buildRollers(width, height, depth, frameH, railW, rollWidth) {
|
|
118
|
+
// 롤러 반지름: rollWidth 속성으로 결정 (기본 10 → 직경 10)
|
|
119
|
+
const rollerRadius = Math.max(rollWidth / 2, 1);
|
|
120
|
+
const rollerLength = height - railW * 2 - 0.5;
|
|
121
|
+
const diameter = rollerRadius * 2;
|
|
122
|
+
// 롤러 간 최소 간격 (Z-fighting 방지)
|
|
123
|
+
const step = diameter + 1;
|
|
124
|
+
// 롤러 중심 = 프레임 중심 (프레임이 롤러를 감싸도록)
|
|
125
|
+
const rollerY = depth / 2 - frameH / 2;
|
|
126
|
+
const rollerGeometries = [];
|
|
127
|
+
const count = Math.max(1, Math.floor(width / step));
|
|
128
|
+
const totalSpan = (count - 1) * step;
|
|
129
|
+
const startX = -totalSpan / 2;
|
|
130
|
+
for (let i = 0; i < count; i++) {
|
|
131
|
+
const x = startX + i * step;
|
|
132
|
+
const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16);
|
|
133
|
+
roller.rotateX(Math.PI / 2);
|
|
134
|
+
roller.translate(x, rollerY, 0);
|
|
135
|
+
rollerGeometries.push(roller);
|
|
136
|
+
}
|
|
137
|
+
if (rollerGeometries.length > 0) {
|
|
138
|
+
const rollerMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(rollerGeometries), new THREE.MeshStandardMaterial({ color: ROLLER_COLOR, metalness: 0.9, roughness: 0.2 }));
|
|
139
|
+
rollerMesh.castShadow = true;
|
|
140
|
+
this.object3d.add(rollerMesh);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
buildBelt(width, height, depth, frameH, railW, value) {
|
|
144
|
+
const drumRadius = frameH * 0.38;
|
|
145
|
+
const drumLength = height - railW * 2;
|
|
146
|
+
const beltY = depth / 2 - frameH / 2;
|
|
147
|
+
const fillColor = FILL_COLORS[value] ?? FILL_COLORS[0];
|
|
148
|
+
const rollerMaterial = new THREE.MeshStandardMaterial({
|
|
149
|
+
color: ROLLER_COLOR,
|
|
150
|
+
metalness: 0.9,
|
|
151
|
+
roughness: 0.2
|
|
152
|
+
});
|
|
153
|
+
// Two end drums
|
|
154
|
+
const drumGeometries = [];
|
|
155
|
+
const drumInset = drumRadius * 1.5;
|
|
156
|
+
const leftDrumX = -width / 2 + drumInset;
|
|
157
|
+
const rightDrumX = width / 2 - drumInset;
|
|
158
|
+
for (const dx of [leftDrumX, rightDrumX]) {
|
|
159
|
+
const drum = new THREE.CylinderGeometry(drumRadius, drumRadius, drumLength, 16);
|
|
160
|
+
drum.rotateX(Math.PI / 2);
|
|
161
|
+
drum.translate(dx, beltY, 0);
|
|
162
|
+
drumGeometries.push(drum);
|
|
163
|
+
}
|
|
164
|
+
const drumMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(drumGeometries), rollerMaterial);
|
|
165
|
+
drumMesh.castShadow = true;
|
|
166
|
+
this.object3d.add(drumMesh);
|
|
167
|
+
// Belt surface: flat top + half-cylinder wraps around each drum end
|
|
168
|
+
const beltThickness = drumRadius * 0.12;
|
|
169
|
+
const beltMaterial = new THREE.MeshStandardMaterial({
|
|
170
|
+
color: fillColor,
|
|
171
|
+
metalness: 0.0,
|
|
172
|
+
roughness: 0.9
|
|
173
|
+
});
|
|
174
|
+
// Flat belt top surface spanning between drums
|
|
175
|
+
const flatLen = rightDrumX - leftDrumX;
|
|
176
|
+
const flatGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength);
|
|
177
|
+
const flatMesh = new THREE.Mesh(flatGeo, beltMaterial);
|
|
178
|
+
flatMesh.position.set(0, beltY + drumRadius, 0);
|
|
179
|
+
flatMesh.castShadow = true;
|
|
180
|
+
this.object3d.add(flatMesh);
|
|
181
|
+
// Belt wrap around drums (half-torus shape using extruded half-circle)
|
|
182
|
+
const wrapSegments = 12;
|
|
183
|
+
for (const [dx, angleStart] of [
|
|
184
|
+
[leftDrumX, Math.PI / 2],
|
|
185
|
+
[rightDrumX, -Math.PI / 2]
|
|
186
|
+
]) {
|
|
187
|
+
const wrapGeometries = [];
|
|
188
|
+
for (let i = 0; i < wrapSegments; i++) {
|
|
189
|
+
const a0 = angleStart + (Math.PI * i) / wrapSegments;
|
|
190
|
+
const a1 = angleStart + (Math.PI * (i + 1)) / wrapSegments;
|
|
191
|
+
const r = drumRadius + beltThickness / 2;
|
|
192
|
+
const x0 = dx + Math.cos(a0) * r;
|
|
193
|
+
const y0 = beltY + Math.sin(a0) * r;
|
|
194
|
+
const x1 = dx + Math.cos(a1) * r;
|
|
195
|
+
const y1 = beltY + Math.sin(a1) * r;
|
|
196
|
+
const segLen = Math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) * 1.05;
|
|
197
|
+
const segAngle = Math.atan2(y1 - y0, x1 - x0);
|
|
198
|
+
const seg = new THREE.BoxGeometry(segLen, beltThickness, drumLength);
|
|
199
|
+
seg.rotateZ(segAngle);
|
|
200
|
+
seg.translate((x0 + x1) / 2, (y0 + y1) / 2, 0);
|
|
201
|
+
wrapGeometries.push(seg);
|
|
202
|
+
}
|
|
203
|
+
const wrapMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wrapGeometries), beltMaterial);
|
|
204
|
+
wrapMesh.castShadow = true;
|
|
205
|
+
this.object3d.add(wrapMesh);
|
|
206
|
+
}
|
|
207
|
+
// Flat belt bottom (return path, slightly below)
|
|
208
|
+
const bottomGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85);
|
|
209
|
+
const bottomMesh = new THREE.Mesh(bottomGeo, beltMaterial);
|
|
210
|
+
bottomMesh.position.set(0, beltY - drumRadius, 0);
|
|
211
|
+
this.object3d.add(bottomMesh);
|
|
212
|
+
}
|
|
213
|
+
/** Control box (motor/controller housing) + status indicator light */
|
|
214
|
+
buildControlBox(width, height, depth, frameH, railW, value) {
|
|
215
|
+
const boxW = Math.max(width * 0.08, 4);
|
|
216
|
+
const boxH = frameH * 0.7;
|
|
217
|
+
const boxD = Math.max(height * 0.2, 4);
|
|
218
|
+
const railY = depth / 2 - frameH / 2;
|
|
219
|
+
// Box positioned at one end, attached to the side rail
|
|
220
|
+
const boxMaterial = new THREE.MeshStandardMaterial({
|
|
221
|
+
color: CONTROL_BOX_COLOR,
|
|
222
|
+
metalness: 0.6,
|
|
223
|
+
roughness: 0.4
|
|
224
|
+
});
|
|
225
|
+
const boxGeo = new THREE.BoxGeometry(boxW, boxH, boxD);
|
|
226
|
+
const boxMesh = new THREE.Mesh(boxGeo, boxMaterial);
|
|
227
|
+
boxMesh.position.set(-width / 2 + boxW / 2 + railW, railY - frameH / 2 - boxH / 2 + boxH * 0.15, -height / 2 + railW + boxD / 2);
|
|
228
|
+
boxMesh.castShadow = true;
|
|
229
|
+
this.object3d.add(boxMesh);
|
|
230
|
+
// Status indicator light on top of control box
|
|
231
|
+
const lightR = Math.min(boxW, boxD) * 0.25;
|
|
232
|
+
const lightH = lightR * 1.2;
|
|
233
|
+
const emissiveColor = STATUS_EMISSIVE[value] ?? STATUS_EMISSIVE[0];
|
|
234
|
+
const lightIntensity = value > 0 ? 1.5 : 0.2;
|
|
235
|
+
const lightMaterial = new THREE.MeshStandardMaterial({
|
|
236
|
+
color: emissiveColor,
|
|
237
|
+
emissive: emissiveColor,
|
|
238
|
+
emissiveIntensity: lightIntensity,
|
|
239
|
+
metalness: 0.0,
|
|
240
|
+
roughness: 0.3
|
|
241
|
+
});
|
|
242
|
+
const lightGeo = new THREE.CylinderGeometry(lightR, lightR * 0.8, lightH, 12);
|
|
243
|
+
const lightMesh = new THREE.Mesh(lightGeo, lightMaterial);
|
|
244
|
+
lightMesh.position.set(boxMesh.position.x, boxMesh.position.y + boxH / 2 + lightH / 2, boxMesh.position.z);
|
|
245
|
+
this.object3d.add(lightMesh);
|
|
246
|
+
}
|
|
247
|
+
updateDimension() { }
|
|
248
|
+
onchange(after, before) {
|
|
249
|
+
if ('value' in after || 'conveyorType' in after || 'rollWidth' in after || 'width' in after || 'height' in after || 'depth' in after) {
|
|
250
|
+
this.update();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
super.onchange(after, before);
|
|
254
|
+
}
|
|
255
|
+
updateAlpha() { }
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=conveyor-3d.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conveyor-3d.js","sourceRoot":"","sources":["../src/conveyor-3d.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,KAAK,mBAAmB,MAAM,iDAAiD,CAAA;AACtF,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AAExD,MAAM,WAAW,GAA2B;IAC1C,CAAC,EAAE,QAAQ,EAAE,OAAO;IACpB,CAAC,EAAE,QAAQ,EAAE,MAAM;IACnB,CAAC,EAAE,QAAQ,EAAE,UAAU;IACvB,CAAC,EAAE,QAAQ,EAAE,OAAO;IACpB,CAAC,EAAE,QAAQ,CAAC,QAAQ;CACrB,CAAA;AAED,MAAM,WAAW,GAAG,QAAQ,CAAA;AAC5B,MAAM,YAAY,GAAG,QAAQ,CAAA;AAC7B,MAAM,iBAAiB,GAAG,QAAQ,CAAA;AAClC,MAAM,eAAe,GAA2B;IAC9C,CAAC,EAAE,QAAQ;IACX,CAAC,EAAE,QAAQ;IACX,CAAC,EAAE,QAAQ;IACX,CAAC,EAAE,QAAQ;IACX,CAAC,EAAE,QAAQ;CACZ,CAAA;AAED,MAAM,OAAO,UAAW,SAAQ,eAAe;IAC7C,IAAI,cAAc;QAChB,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAA;QACrD,OAAO,KAAK,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,GAAG,GAAG,CAAA;IAC/C,CAAC;IAED,IAAI,QAAQ;QACV,MAAM,EAAE,IAAI,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAA;QACzC,OAAO;YACL,CAAC,EAAE,IAAI,CAAC,EAAE;YACV,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC,cAAc,GAAG,CAAC;YACjC,CAAC,EAAE,IAAI,CAAC,EAAE;SACX,CAAA;IACH,CAAC;IAED;;;OAGG;IACH,IAAc,cAAc;QAC1B,OAAO,IAAI,CAAC,cAAc,GAAG,CAAC,CAAA;IAChC,CAAC;IAED,KAAK;QACH,KAAK,CAAC,KAAK,EAAE,CAAA;QAEb,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,SAAS,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAA;QAC3F,MAAM,KAAK,GAAG,IAAI,CAAC,cAAc,CAAA;QAEjC,oDAAoD;QACpD,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,SAAmB,EAAE,CAAC,CAAC,CAAA;QACvD,MAAM,MAAM,GAAG,cAAc,CAAA;QAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,MAAM,EAAE,CAAC,CAAC,CAAA;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE,CAAC,CAAC,CAAA;QACxC,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,GAAG,EAAE,CAAC,CAAC,CAAA;QAE7C,MAAM,aAAa,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YACnD,KAAK,EAAE,WAAW;YAClB,SAAS,EAAE,IAAI;YACf,SAAS,EAAE,IAAI;SAChB,CAAC,CAAA;QAEF,mDAAmD;QACnD,MAAM,eAAe,GAA2B,EAAE,CAAA;QAClD,MAAM,KAAK,GAAG,KAAK,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,CAAA;QAEpC,uBAAuB;QACvB,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,CAAA;YACxD,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAA;YAC1D,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC5B,CAAC;QAED,mCAAmC;QACnC,MAAM,OAAO,GAAG,KAAK,GAAG,CAAC,GAAG,MAAM,CAAA;QAClC,MAAM,UAAU,GAAG,OAAO,GAAG,IAAI,GAAG,CAAC,CAAA;QAErC,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YAC5B,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC5B,MAAM,EAAE,GAAG,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,YAAY,GAAG,CAAC,CAAC,CAAA;gBACjD,MAAM,EAAE,GAAG,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAA;gBAC3C,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,YAAY,EAAE,IAAI,EAAE,YAAY,CAAC,CAAA;gBACnE,GAAG,CAAC,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;gBACjC,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YAC3B,CAAC;QACH,CAAC;QAED,0BAA0B;QAC1B,MAAM,MAAM,GAAG,YAAY,GAAG,GAAG,CAAA;QACjC,MAAM,MAAM,GAAG,YAAY,GAAG,GAAG,CAAA;QACjC,MAAM,QAAQ,GAAG,MAAM,GAAG,KAAK,CAAA;QAC/B,MAAM,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,IAAI,CAAA;QAEpC,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YAC5B,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAA;YAC7D,KAAK,CAAC,SAAS,CAAC,KAAK,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,YAAY,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;YAClE,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC7B,CAAC;QAED,0BAA0B;QAC1B,MAAM,SAAS,GAAG,KAAK,GAAG,YAAY,CAAA;QACtC,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;YAC5B,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;YAC9D,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,MAAM,EAAE,KAAK,GAAG,CAAC,MAAM,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAA;YAC5D,eAAe,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC7B,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,eAAe,CAAC,eAAe,CAAC,EAAE,aAAa,CAAC,CAAA;QACrG,SAAS,CAAC,UAAU,GAAG,IAAI,CAAA;QAC3B,SAAS,CAAC,aAAa,GAAG,IAAI,CAAA;QAC9B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QAE5B,0BAA0B;QAC1B,IAAI,YAAY,KAAK,CAAC,EAAE,CAAC;YACvB,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;QAC5D,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAmB,CAAC,CAAA;QAC7E,CAAC;QAED,iDAAiD;QACjD,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;IAClE,CAAC;IAEO,YAAY,CAAC,KAAa,EAAE,MAAc,EAAE,KAAa,EAAE,MAAc,EAAE,KAAa,EAAE,SAAiB;QACjH,4CAA4C;QAC5C,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,SAAS,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;QAC/C,MAAM,YAAY,GAAG,MAAM,GAAG,KAAK,GAAG,CAAC,GAAG,GAAG,CAAA;QAC7C,MAAM,QAAQ,GAAG,YAAY,GAAG,CAAC,CAAA;QACjC,6BAA6B;QAC7B,MAAM,IAAI,GAAG,QAAQ,GAAG,CAAC,CAAA;QACzB,iCAAiC;QACjC,MAAM,OAAO,GAAG,KAAK,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,CAAA;QAEtC,MAAM,gBAAgB,GAA2B,EAAE,CAAA;QACnD,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAA;QACnD,MAAM,SAAS,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,IAAI,CAAA;QACpC,MAAM,MAAM,GAAG,CAAC,SAAS,GAAG,CAAC,CAAA;QAE7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/B,MAAM,CAAC,GAAG,MAAM,GAAG,CAAC,GAAG,IAAI,CAAA;YAC3B,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,gBAAgB,CAAC,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,EAAE,CAAC,CAAA;YACvF,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;YAC3B,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;YAC/B,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC/B,CAAC;QAED,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChC,MAAM,UAAU,GAAG,IAAI,KAAK,CAAC,IAAI,CAC/B,mBAAmB,CAAC,eAAe,CAAC,gBAAgB,CAAC,EACrD,IAAI,KAAK,CAAC,oBAAoB,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CACxF,CAAA;YACD,UAAU,CAAC,UAAU,GAAG,IAAI,CAAA;YAC5B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;QAC/B,CAAC;IACH,CAAC;IAEO,SAAS,CAAC,KAAa,EAAE,MAAc,EAAE,KAAa,EAAE,MAAc,EAAE,KAAa,EAAE,KAAa;QAC1G,MAAM,UAAU,GAAG,MAAM,GAAG,IAAI,CAAA;QAChC,MAAM,UAAU,GAAG,MAAM,GAAG,KAAK,GAAG,CAAC,CAAA;QACrC,MAAM,KAAK,GAAG,KAAK,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,CAAA;QACpC,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC,CAAC,CAAC,CAAA;QAEtD,MAAM,cAAc,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YACpD,KAAK,EAAE,YAAY;YACnB,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;SACf,CAAC,CAAA;QAEF,gBAAgB;QAChB,MAAM,cAAc,GAA2B,EAAE,CAAA;QACjD,MAAM,SAAS,GAAG,UAAU,GAAG,GAAG,CAAA;QAClC,MAAM,SAAS,GAAG,CAAC,KAAK,GAAG,CAAC,GAAG,SAAS,CAAA;QACxC,MAAM,UAAU,GAAG,KAAK,GAAG,CAAC,GAAG,SAAS,CAAA;QAExC,KAAK,MAAM,EAAE,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,GAAG,IAAI,KAAK,CAAC,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;YAC/E,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,CAAA;YACzB,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;YAC5B,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC3B,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,eAAe,CAAC,cAAc,CAAC,EAAE,cAAc,CAAC,CAAA;QACpG,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAA;QAC1B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAE3B,oEAAoE;QACpE,MAAM,aAAa,GAAG,UAAU,GAAG,IAAI,CAAA;QACvC,MAAM,YAAY,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YAClD,KAAK,EAAE,SAAS;YAChB,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;SACf,CAAC,CAAA;QAEF,+CAA+C;QAC/C,MAAM,OAAO,GAAG,UAAU,GAAG,SAAS,CAAA;QACtC,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,UAAU,CAAC,CAAA;QACzE,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;QACtD,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,UAAU,EAAE,CAAC,CAAC,CAAA;QAC/C,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAA;QAC1B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAE3B,uEAAuE;QACvE,MAAM,YAAY,GAAG,EAAE,CAAA;QACvB,KAAK,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,IAAI;YAC7B,CAAC,SAAS,EAAE,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;YACxB,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;SACL,EAAE,CAAC;YACxB,MAAM,cAAc,GAA2B,EAAE,CAAA;YACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC;gBACtC,MAAM,EAAE,GAAG,UAAU,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,YAAY,CAAA;gBACpD,MAAM,EAAE,GAAG,UAAU,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,YAAY,CAAA;gBAC1D,MAAM,CAAC,GAAG,UAAU,GAAG,aAAa,GAAG,CAAC,CAAA;gBAExC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;gBAChC,MAAM,EAAE,GAAG,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;gBACnC,MAAM,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;gBAChC,MAAM,EAAE,GAAG,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;gBAEnC,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,CAAA;gBAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAA;gBAE7C,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,CAAC,CAAA;gBACpE,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;gBACrB,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;gBAC9C,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YAC1B,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,eAAe,CAAC,cAAc,CAAC,EAAE,YAAY,CAAC,CAAA;YAClG,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAA;YAC1B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAC7B,CAAC;QAED,iDAAiD;QACjD,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,aAAa,EAAE,UAAU,GAAG,IAAI,CAAC,CAAA;QAClF,MAAM,UAAU,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,YAAY,CAAC,CAAA;QAC1D,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,UAAU,EAAE,CAAC,CAAC,CAAA;QACjD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IAC/B,CAAC;IAED,sEAAsE;IAC9D,eAAe,CACrB,KAAa,EACb,MAAc,EACd,KAAa,EACb,MAAc,EACd,KAAa,EACb,KAAa;QAEb,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,EAAE,CAAC,CAAC,CAAA;QACtC,MAAM,IAAI,GAAG,MAAM,GAAG,GAAG,CAAA;QACzB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC,CAAC,CAAA;QACtC,MAAM,KAAK,GAAG,KAAK,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,CAAA;QAEpC,uDAAuD;QACvD,MAAM,WAAW,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YACjD,KAAK,EAAE,iBAAiB;YACxB,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;SACf,CAAC,CAAA;QAEF,MAAM,MAAM,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAA;QACtD,MAAM,OAAO,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,CAAA;QACnD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAClB,CAAC,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,KAAK,EAC7B,KAAK,GAAG,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,EAC3C,CAAC,MAAM,GAAG,CAAC,GAAG,KAAK,GAAG,IAAI,GAAG,CAAC,CAC/B,CAAA;QACD,OAAO,CAAC,UAAU,GAAG,IAAI,CAAA;QACzB,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAE1B,+CAA+C;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,IAAI,CAAA;QAC1C,MAAM,MAAM,GAAG,MAAM,GAAG,GAAG,CAAA;QAC3B,MAAM,aAAa,GAAG,eAAe,CAAC,KAAK,CAAC,IAAI,eAAe,CAAC,CAAC,CAAC,CAAA;QAClE,MAAM,cAAc,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAA;QAE5C,MAAM,aAAa,GAAG,IAAI,KAAK,CAAC,oBAAoB,CAAC;YACnD,KAAK,EAAE,aAAa;YACpB,QAAQ,EAAE,aAAa;YACvB,iBAAiB,EAAE,cAAc;YACjC,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG;SACf,CAAC,CAAA;QAEF,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,CAAA;QAC7E,MAAM,SAAS,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAA;QACzD,SAAS,CAAC,QAAQ,CAAC,GAAG,CACpB,OAAO,CAAC,QAAQ,CAAC,CAAC,EAClB,OAAO,CAAC,QAAQ,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,MAAM,GAAG,CAAC,EAC1C,OAAO,CAAC,QAAQ,CAAC,CAAC,CACnB,CAAA;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;IAC9B,CAAC;IAED,eAAe,KAAI,CAAC;IAEpB,QAAQ,CAAC,KAA8B,EAAE,MAA+B;QACtE,IAAI,OAAO,IAAI,KAAK,IAAI,cAAc,IAAI,KAAK,IAAI,WAAW,IAAI,KAAK,IAAI,OAAO,IAAI,KAAK,IAAI,QAAQ,IAAI,KAAK,IAAI,OAAO,IAAI,KAAK,EAAE,CAAC;YACrI,IAAI,CAAC,MAAM,EAAE,CAAA;YACb,OAAM;QACR,CAAC;QACD,KAAK,CAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;IAC/B,CAAC;IAED,WAAW,KAAI,CAAC;CACjB","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Straight Conveyor 3D Model\n *\n * Roller type: side rails + evenly-spaced cylindrical rollers + 4 legs with cross-bracing\n * Belt type: side rails + two end drums with belt wrapping over them + 4 legs with cross-bracing\n * Both types include a control box at one end and a status indicator light.\n */\n\nimport * as THREE from 'three'\nimport * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js'\nimport { RealObjectGroup } from '@hatiolab/things-scene'\n\nconst FILL_COLORS: Record<number, number> = {\n 0: 0xcccccc, // IDLE\n 1: 0xafd0f1, // RUN\n 2: 0xafd0f1, // REVERSE\n 3: 0xffba00, // WARN\n 4: 0xe9746b // ERROR\n}\n\nconst FRAME_COLOR = 0x888899\nconst ROLLER_COLOR = 0xaaaabc\nconst CONTROL_BOX_COLOR = 0x556677\nconst STATUS_EMISSIVE: Record<number, number> = {\n 0: 0x333333,\n 1: 0x44aaff,\n 2: 0x44aaff,\n 3: 0xffaa00,\n 4: 0xff3333\n}\n\nexport class Conveyor3D extends RealObjectGroup {\n get effectiveDepth(): number {\n const { width, height, depth } = this.component.state\n return depth || Math.min(width, height) * 1.5\n }\n\n get position() {\n const { zPos = 0 } = this.component.state\n return {\n x: this.cx,\n y: zPos + this.effectiveDepth / 2,\n z: this.cy\n }\n }\n\n /**\n * syncFromObject3D 역변환 시 zPos 오프셋.\n * position.y = zPos + effectiveDepth/2 이므로, 역변환도 effectiveDepth/2를 빼야 한다.\n */\n protected get syncZPosOffset(): number {\n return this.effectiveDepth / 2\n }\n\n build() {\n super.build()\n\n const { width, height, conveyorType = 0, value = 0, rollWidth = 10 } = this.component.state\n const depth = this.effectiveDepth\n\n // 프레임 두께: rollWidth(롤러 직경)와 동일 단위. 롤러를 감쌀 수 있어야 한다.\n const rollerDiameter = Math.max(rollWidth as number, 2)\n const frameH = rollerDiameter\n const legH = Math.max(depth - frameH, 0)\n const railW = Math.max(height * 0.06, 2)\n const legThickness = Math.max(railW * 0.8, 2)\n\n const frameMaterial = new THREE.MeshStandardMaterial({\n color: FRAME_COLOR,\n metalness: 0.85,\n roughness: 0.35\n })\n\n // --- Frame: side rails + legs + cross-bracing ---\n const frameGeometries: THREE.BufferGeometry[] = []\n const railY = depth / 2 - frameH / 2\n\n // Side rails (along X)\n for (const zSign of [-1, 1]) {\n const rail = new THREE.BoxGeometry(width, frameH, railW)\n rail.translate(0, railY, zSign * (height / 2 - railW / 2))\n frameGeometries.push(rail)\n }\n\n // 4 legs at corners — 레일 바로 아래에 정렬\n const legTopY = depth / 2 - frameH\n const legCenterY = legTopY - legH / 2\n\n for (const xSign of [-1, 1]) {\n for (const zSign of [-1, 1]) {\n const lx = xSign * (width / 2 - legThickness / 2)\n const lz = zSign * (height / 2 - railW / 2)\n const leg = new THREE.BoxGeometry(legThickness, legH, legThickness)\n leg.translate(lx, legCenterY, lz)\n frameGeometries.push(leg)\n }\n }\n\n // Cross-bracing: 다리 사이 연결\n const braceH = legThickness * 0.6\n const braceW = legThickness * 0.6\n const braceLen = height - railW\n const braceY = legTopY - legH * 0.35\n\n for (const xSign of [-1, 1]) {\n const brace = new THREE.BoxGeometry(braceW, braceH, braceLen)\n brace.translate(xSign * (width / 2 - legThickness / 2), braceY, 0)\n frameGeometries.push(brace)\n }\n\n // Cross-bracing: 좌우 다리 연결\n const braceLenX = width - legThickness\n for (const zSign of [-1, 1]) {\n const brace = new THREE.BoxGeometry(braceLenX, braceH, braceW)\n brace.translate(0, braceY, zSign * (height / 2 - railW / 2))\n frameGeometries.push(brace)\n }\n\n const frameMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(frameGeometries), frameMaterial)\n frameMesh.castShadow = true\n frameMesh.receiveShadow = true\n this.object3d.add(frameMesh)\n\n // --- Rollers or Belt ---\n if (conveyorType === 1) {\n this.buildBelt(width, height, depth, frameH, railW, value)\n } else {\n this.buildRollers(width, height, depth, frameH, railW, rollWidth as number)\n }\n\n // --- Control box (motor housing at one end) ---\n this.buildControlBox(width, height, depth, frameH, railW, value)\n }\n\n private buildRollers(width: number, height: number, depth: number, frameH: number, railW: number, rollWidth: number) {\n // 롤러 반지름: rollWidth 속성으로 결정 (기본 10 → 직경 10)\n const rollerRadius = Math.max(rollWidth / 2, 1)\n const rollerLength = height - railW * 2 - 0.5\n const diameter = rollerRadius * 2\n // 롤러 간 최소 간격 (Z-fighting 방지)\n const step = diameter + 1\n // 롤러 중심 = 프레임 중심 (프레임이 롤러를 감싸도록)\n const rollerY = depth / 2 - frameH / 2\n\n const rollerGeometries: THREE.BufferGeometry[] = []\n const count = Math.max(1, Math.floor(width / step))\n const totalSpan = (count - 1) * step\n const startX = -totalSpan / 2\n\n for (let i = 0; i < count; i++) {\n const x = startX + i * step\n const roller = new THREE.CylinderGeometry(rollerRadius, rollerRadius, rollerLength, 16)\n roller.rotateX(Math.PI / 2)\n roller.translate(x, rollerY, 0)\n rollerGeometries.push(roller)\n }\n\n if (rollerGeometries.length > 0) {\n const rollerMesh = new THREE.Mesh(\n BufferGeometryUtils.mergeGeometries(rollerGeometries),\n new THREE.MeshStandardMaterial({ color: ROLLER_COLOR, metalness: 0.9, roughness: 0.2 })\n )\n rollerMesh.castShadow = true\n this.object3d.add(rollerMesh)\n }\n }\n\n private buildBelt(width: number, height: number, depth: number, frameH: number, railW: number, value: number) {\n const drumRadius = frameH * 0.38\n const drumLength = height - railW * 2\n const beltY = depth / 2 - frameH / 2\n const fillColor = FILL_COLORS[value] ?? FILL_COLORS[0]\n\n const rollerMaterial = new THREE.MeshStandardMaterial({\n color: ROLLER_COLOR,\n metalness: 0.9,\n roughness: 0.2\n })\n\n // Two end drums\n const drumGeometries: THREE.BufferGeometry[] = []\n const drumInset = drumRadius * 1.5\n const leftDrumX = -width / 2 + drumInset\n const rightDrumX = width / 2 - drumInset\n\n for (const dx of [leftDrumX, rightDrumX]) {\n const drum = new THREE.CylinderGeometry(drumRadius, drumRadius, drumLength, 16)\n drum.rotateX(Math.PI / 2)\n drum.translate(dx, beltY, 0)\n drumGeometries.push(drum)\n }\n\n const drumMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(drumGeometries), rollerMaterial)\n drumMesh.castShadow = true\n this.object3d.add(drumMesh)\n\n // Belt surface: flat top + half-cylinder wraps around each drum end\n const beltThickness = drumRadius * 0.12\n const beltMaterial = new THREE.MeshStandardMaterial({\n color: fillColor,\n metalness: 0.0,\n roughness: 0.9\n })\n\n // Flat belt top surface spanning between drums\n const flatLen = rightDrumX - leftDrumX\n const flatGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength)\n const flatMesh = new THREE.Mesh(flatGeo, beltMaterial)\n flatMesh.position.set(0, beltY + drumRadius, 0)\n flatMesh.castShadow = true\n this.object3d.add(flatMesh)\n\n // Belt wrap around drums (half-torus shape using extruded half-circle)\n const wrapSegments = 12\n for (const [dx, angleStart] of [\n [leftDrumX, Math.PI / 2],\n [rightDrumX, -Math.PI / 2]\n ] as [number, number][]) {\n const wrapGeometries: THREE.BufferGeometry[] = []\n for (let i = 0; i < wrapSegments; i++) {\n const a0 = angleStart + (Math.PI * i) / wrapSegments\n const a1 = angleStart + (Math.PI * (i + 1)) / wrapSegments\n const r = drumRadius + beltThickness / 2\n\n const x0 = dx + Math.cos(a0) * r\n const y0 = beltY + Math.sin(a0) * r\n const x1 = dx + Math.cos(a1) * r\n const y1 = beltY + Math.sin(a1) * r\n\n const segLen = Math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2) * 1.05\n const segAngle = Math.atan2(y1 - y0, x1 - x0)\n\n const seg = new THREE.BoxGeometry(segLen, beltThickness, drumLength)\n seg.rotateZ(segAngle)\n seg.translate((x0 + x1) / 2, (y0 + y1) / 2, 0)\n wrapGeometries.push(seg)\n }\n const wrapMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(wrapGeometries), beltMaterial)\n wrapMesh.castShadow = true\n this.object3d.add(wrapMesh)\n }\n\n // Flat belt bottom (return path, slightly below)\n const bottomGeo = new THREE.BoxGeometry(flatLen, beltThickness, drumLength * 0.85)\n const bottomMesh = new THREE.Mesh(bottomGeo, beltMaterial)\n bottomMesh.position.set(0, beltY - drumRadius, 0)\n this.object3d.add(bottomMesh)\n }\n\n /** Control box (motor/controller housing) + status indicator light */\n private buildControlBox(\n width: number,\n height: number,\n depth: number,\n frameH: number,\n railW: number,\n value: number\n ) {\n const boxW = Math.max(width * 0.08, 4)\n const boxH = frameH * 0.7\n const boxD = Math.max(height * 0.2, 4)\n const railY = depth / 2 - frameH / 2\n\n // Box positioned at one end, attached to the side rail\n const boxMaterial = new THREE.MeshStandardMaterial({\n color: CONTROL_BOX_COLOR,\n metalness: 0.6,\n roughness: 0.4\n })\n\n const boxGeo = new THREE.BoxGeometry(boxW, boxH, boxD)\n const boxMesh = new THREE.Mesh(boxGeo, boxMaterial)\n boxMesh.position.set(\n -width / 2 + boxW / 2 + railW,\n railY - frameH / 2 - boxH / 2 + boxH * 0.15,\n -height / 2 + railW + boxD / 2\n )\n boxMesh.castShadow = true\n this.object3d.add(boxMesh)\n\n // Status indicator light on top of control box\n const lightR = Math.min(boxW, boxD) * 0.25\n const lightH = lightR * 1.2\n const emissiveColor = STATUS_EMISSIVE[value] ?? STATUS_EMISSIVE[0]\n const lightIntensity = value > 0 ? 1.5 : 0.2\n\n const lightMaterial = new THREE.MeshStandardMaterial({\n color: emissiveColor,\n emissive: emissiveColor,\n emissiveIntensity: lightIntensity,\n metalness: 0.0,\n roughness: 0.3\n })\n\n const lightGeo = new THREE.CylinderGeometry(lightR, lightR * 0.8, lightH, 12)\n const lightMesh = new THREE.Mesh(lightGeo, lightMaterial)\n lightMesh.position.set(\n boxMesh.position.x,\n boxMesh.position.y + boxH / 2 + lightH / 2,\n boxMesh.position.z\n )\n this.object3d.add(lightMesh)\n }\n\n updateDimension() {}\n\n onchange(after: Record<string, unknown>, before: Record<string, unknown>) {\n if ('value' in after || 'conveyorType' in after || 'rollWidth' in after || 'width' in after || 'height' in after || 'depth' in after) {\n this.update()\n return\n }\n super.onchange(after, before)\n }\n\n updateAlpha() {}\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { RealObjectGroup } from '@hatiolab/things-scene';
|
|
2
|
+
export declare class ConveyorJoin3D extends RealObjectGroup {
|
|
3
|
+
get effectiveDepth(): number;
|
|
4
|
+
get position(): {
|
|
5
|
+
x: number;
|
|
6
|
+
y: any;
|
|
7
|
+
z: number;
|
|
8
|
+
};
|
|
9
|
+
build(): void;
|
|
10
|
+
private buildArcRail;
|
|
11
|
+
private arcPoint;
|
|
12
|
+
private buildRollers;
|
|
13
|
+
private buildBelt;
|
|
14
|
+
/** Control box + status light positioned at outer edge near start angle */
|
|
15
|
+
private buildControlBox;
|
|
16
|
+
updateDimension(): void;
|
|
17
|
+
onchange(after: Record<string, unknown>, before: Record<string, unknown>): void;
|
|
18
|
+
updateAlpha(): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Curved Conveyor (ConveyorJoin) 3D Model
|
|
5
|
+
* Reference: Kramer & Duyvis Round Belt Conveyor Type R
|
|
6
|
+
*
|
|
7
|
+
* A curved conveyor section that changes direction of travel (e.g., 90° turn).
|
|
8
|
+
* Key feature: conical (tapered) rollers — wider at outer radius, narrower at
|
|
9
|
+
* inner radius — ensuring uniform belt speed across the curve width.
|
|
10
|
+
* Structure: curved side rails + conical rollers/belt + end caps + legs
|
|
11
|
+
* + cross-bracing + control box with status light.
|
|
12
|
+
*/
|
|
13
|
+
import * as THREE from 'three';
|
|
14
|
+
import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
15
|
+
import { RealObjectGroup } from '@hatiolab/things-scene';
|
|
16
|
+
const FILL_COLORS = {
|
|
17
|
+
0: 0xcccccc, // IDLE
|
|
18
|
+
1: 0xafd0f1, // RUN
|
|
19
|
+
2: 0xafd0f1, // REVERSE
|
|
20
|
+
3: 0xffba00, // WARN
|
|
21
|
+
4: 0xe9746b // ERROR
|
|
22
|
+
};
|
|
23
|
+
const FRAME_COLOR = 0x888899;
|
|
24
|
+
const ROLLER_COLOR = 0xaaaabc;
|
|
25
|
+
const CONTROL_BOX_COLOR = 0x556677;
|
|
26
|
+
const STATUS_EMISSIVE = {
|
|
27
|
+
0: 0x333333,
|
|
28
|
+
1: 0x44aaff,
|
|
29
|
+
2: 0x44aaff,
|
|
30
|
+
3: 0xffaa00,
|
|
31
|
+
4: 0xff3333
|
|
32
|
+
};
|
|
33
|
+
export class ConveyorJoin3D extends RealObjectGroup {
|
|
34
|
+
get effectiveDepth() {
|
|
35
|
+
const { rx = 100, ratio = 50, depth } = this.component.state;
|
|
36
|
+
const outerR = Math.abs(rx);
|
|
37
|
+
const beltWidth = outerR - (outerR * Math.abs(ratio)) / 100;
|
|
38
|
+
return depth || beltWidth * 1.5;
|
|
39
|
+
}
|
|
40
|
+
get position() {
|
|
41
|
+
const { zPos = 0 } = this.component.state;
|
|
42
|
+
return {
|
|
43
|
+
x: this.cx,
|
|
44
|
+
y: zPos + this.effectiveDepth / 2,
|
|
45
|
+
z: this.cy
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
build() {
|
|
49
|
+
super.build();
|
|
50
|
+
const { rx: _rx = 100, ry: _ry = 100, ratio = 50, startAngle = 0, endAngle = Math.PI / 2, value = 0, conveyorType = 0, rollWidth } = this.component.state;
|
|
51
|
+
const depth = this.effectiveDepth;
|
|
52
|
+
const outerRx = Math.abs(_rx);
|
|
53
|
+
const outerRy = Math.abs(_ry);
|
|
54
|
+
const innerRx = (outerRx * Math.abs(ratio)) / 100;
|
|
55
|
+
const innerRy = (outerRy * Math.abs(ratio)) / 100;
|
|
56
|
+
const beltWidth = outerRx - innerRx;
|
|
57
|
+
const frameH = depth * 0.15;
|
|
58
|
+
const legH = depth - frameH;
|
|
59
|
+
const railW = Math.max(beltWidth * 0.06, 2);
|
|
60
|
+
const legThickness = Math.max(railW * 0.8, 2);
|
|
61
|
+
const railY = depth / 2 - frameH / 2;
|
|
62
|
+
const arcSpan = endAngle - startAngle;
|
|
63
|
+
const segments = Math.max(12, Math.floor((Math.abs(arcSpan) * outerRx) / 5));
|
|
64
|
+
const frameMaterial = new THREE.MeshStandardMaterial({
|
|
65
|
+
color: FRAME_COLOR,
|
|
66
|
+
metalness: 0.85,
|
|
67
|
+
roughness: 0.35
|
|
68
|
+
});
|
|
69
|
+
// --- Side rails (box segments along inner/outer arcs) ---
|
|
70
|
+
const frameGeometries = [];
|
|
71
|
+
this.buildArcRail(frameGeometries, outerRx, outerRy, railW, startAngle, endAngle, segments, frameH, railY, true);
|
|
72
|
+
this.buildArcRail(frameGeometries, innerRx, innerRy, railW, startAngle, endAngle, segments, frameH, railY, false);
|
|
73
|
+
// End caps (straight segments connecting inner/outer at start and end angles)
|
|
74
|
+
for (const angle of [startAngle, endAngle]) {
|
|
75
|
+
const ox = outerRx * Math.sin(angle);
|
|
76
|
+
const oz = -outerRy * Math.cos(angle);
|
|
77
|
+
const ix = innerRx * Math.sin(angle);
|
|
78
|
+
const iz = -innerRy * Math.cos(angle);
|
|
79
|
+
const capLen = Math.sqrt((ox - ix) ** 2 + (oz - iz) ** 2);
|
|
80
|
+
const capAngle = Math.atan2(ox - ix, -(oz - iz));
|
|
81
|
+
const cap = new THREE.BoxGeometry(capLen, frameH, railW);
|
|
82
|
+
cap.rotateY(-capAngle);
|
|
83
|
+
cap.translate((ox + ix) / 2, railY, (oz + iz) / 2);
|
|
84
|
+
frameGeometries.push(cap);
|
|
85
|
+
}
|
|
86
|
+
// 4 legs at corners + cross-bracing
|
|
87
|
+
const legPositions = [];
|
|
88
|
+
for (const angle of [startAngle, endAngle]) {
|
|
89
|
+
for (const [rx, ry] of [
|
|
90
|
+
[outerRx - railW, outerRy - railW],
|
|
91
|
+
[innerRx + railW, innerRy + railW]
|
|
92
|
+
]) {
|
|
93
|
+
const lx = rx * Math.sin(angle);
|
|
94
|
+
const lz = -ry * Math.cos(angle);
|
|
95
|
+
const leg = new THREE.BoxGeometry(legThickness, legH, legThickness);
|
|
96
|
+
leg.translate(lx, depth / 2 - frameH - legH / 2, lz);
|
|
97
|
+
frameGeometries.push(leg);
|
|
98
|
+
legPositions.push([lx, lz]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Cross-bracing: connect leg pairs at each end (outer-inner) and along each side (start-end)
|
|
102
|
+
const braceSize = legThickness * 0.6;
|
|
103
|
+
const braceY = depth / 2 - frameH - legH * 0.35;
|
|
104
|
+
// Connect outer-to-inner at each end angle
|
|
105
|
+
for (let i = 0; i < legPositions.length; i += 2) {
|
|
106
|
+
const [x0, z0] = legPositions[i];
|
|
107
|
+
const [x1, z1] = legPositions[i + 1];
|
|
108
|
+
const braceLen = Math.sqrt((x1 - x0) ** 2 + (z1 - z0) ** 2);
|
|
109
|
+
const braceAngle = Math.atan2(x1 - x0, -(z1 - z0));
|
|
110
|
+
const brace = new THREE.BoxGeometry(braceLen, braceSize, braceSize);
|
|
111
|
+
brace.rotateY(-braceAngle);
|
|
112
|
+
brace.translate((x0 + x1) / 2, braceY, (z0 + z1) / 2);
|
|
113
|
+
frameGeometries.push(brace);
|
|
114
|
+
}
|
|
115
|
+
// Connect start-to-end along outer side and inner side
|
|
116
|
+
for (const side of [0, 1]) {
|
|
117
|
+
const [x0, z0] = legPositions[side];
|
|
118
|
+
const [x1, z1] = legPositions[side + 2];
|
|
119
|
+
const braceLen = Math.sqrt((x1 - x0) ** 2 + (z1 - z0) ** 2);
|
|
120
|
+
const braceAngle = Math.atan2(x1 - x0, -(z1 - z0));
|
|
121
|
+
const brace = new THREE.BoxGeometry(braceLen, braceSize, braceSize);
|
|
122
|
+
brace.rotateY(-braceAngle);
|
|
123
|
+
brace.translate((x0 + x1) / 2, braceY, (z0 + z1) / 2);
|
|
124
|
+
frameGeometries.push(brace);
|
|
125
|
+
}
|
|
126
|
+
const frameMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(frameGeometries), frameMaterial);
|
|
127
|
+
frameMesh.castShadow = true;
|
|
128
|
+
frameMesh.receiveShadow = true;
|
|
129
|
+
this.object3d.add(frameMesh);
|
|
130
|
+
// --- Rollers or Belt ---
|
|
131
|
+
if (conveyorType === 1) {
|
|
132
|
+
this.buildBelt(outerRx, outerRy, innerRx, innerRy, railW, startAngle, endAngle, segments, depth, frameH, value);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
this.buildRollers(outerRx, outerRy, innerRx, innerRy, railW, startAngle, endAngle, depth, frameH, rollWidth, beltWidth);
|
|
136
|
+
}
|
|
137
|
+
// --- Control box + status light at outer edge near start angle ---
|
|
138
|
+
this.buildControlBox(outerRx, outerRy, innerRx, innerRy, startAngle, depth, frameH, railW, value);
|
|
139
|
+
}
|
|
140
|
+
buildArcRail(geometries, rx, ry, railW, startAngle, endAngle, segments, frameH, railY, _isOuter) {
|
|
141
|
+
const arcSpan = endAngle - startAngle;
|
|
142
|
+
for (let i = 0; i < segments; i++) {
|
|
143
|
+
const t0 = i / segments;
|
|
144
|
+
const t1 = (i + 1) / segments;
|
|
145
|
+
const a0 = startAngle + arcSpan * t0;
|
|
146
|
+
const a1 = startAngle + arcSpan * t1;
|
|
147
|
+
const x0 = rx * Math.sin(a0);
|
|
148
|
+
const z0 = -ry * Math.cos(a0);
|
|
149
|
+
const x1 = rx * Math.sin(a1);
|
|
150
|
+
const z1 = -ry * Math.cos(a1);
|
|
151
|
+
const segLen = Math.sqrt((x1 - x0) ** 2 + (z1 - z0) ** 2) * 1.03;
|
|
152
|
+
const tangentAngle = Math.atan2(x1 - x0, -(z1 - z0));
|
|
153
|
+
const seg = new THREE.BoxGeometry(segLen, frameH, railW);
|
|
154
|
+
seg.rotateY(-tangentAngle);
|
|
155
|
+
seg.translate((x0 + x1) / 2, railY, (z0 + z1) / 2);
|
|
156
|
+
geometries.push(seg);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
arcPoint(rx, ry, angle) {
|
|
160
|
+
return { x: rx * Math.sin(angle), z: -ry * Math.cos(angle) };
|
|
161
|
+
}
|
|
162
|
+
buildRollers(outerRx, outerRy, innerRx, innerRy, railW, startAngle, endAngle, depth, frameH, rollWidth, beltWidth) {
|
|
163
|
+
const rollerRadius = frameH * 0.25;
|
|
164
|
+
const rollerY = depth / 2 - frameH / 2;
|
|
165
|
+
const midRx = (outerRx + innerRx) / 2;
|
|
166
|
+
const arcLength = midRx * Math.abs(endAngle - startAngle);
|
|
167
|
+
const spacing = rollWidth || Math.max(beltWidth * 0.5, 8);
|
|
168
|
+
const count = Math.max(1, Math.floor(arcLength / spacing));
|
|
169
|
+
const rollerMaterial = new THREE.MeshStandardMaterial({
|
|
170
|
+
color: ROLLER_COLOR,
|
|
171
|
+
metalness: 0.9,
|
|
172
|
+
roughness: 0.2
|
|
173
|
+
});
|
|
174
|
+
const rollerGeometries = [];
|
|
175
|
+
for (let i = 1; i <= count; i++) {
|
|
176
|
+
const t = i / (count + 1);
|
|
177
|
+
const angle = startAngle + (endAngle - startAngle) * t;
|
|
178
|
+
const o = this.arcPoint(outerRx - railW, outerRy - railW, angle);
|
|
179
|
+
const inn = this.arcPoint(innerRx + railW, innerRy + railW, angle);
|
|
180
|
+
const rollerLength = Math.sqrt((o.x - inn.x) ** 2 + (o.z - inn.z) ** 2);
|
|
181
|
+
const midX = (o.x + inn.x) / 2;
|
|
182
|
+
const midZ = (o.z + inn.z) / 2;
|
|
183
|
+
const radialAngle = Math.atan2(o.x - inn.x, -(o.z - inn.z));
|
|
184
|
+
// Conical roller: taper capped at 1.5:1
|
|
185
|
+
const outerDist = Math.sqrt(o.x ** 2 + o.z ** 2);
|
|
186
|
+
const innerDist = Math.sqrt(inn.x ** 2 + inn.z ** 2);
|
|
187
|
+
const rawRatio = innerDist > 0 ? outerDist / innerDist : 1;
|
|
188
|
+
const cappedRatio = Math.min(rawRatio, 1.5);
|
|
189
|
+
const outerRollerR = (rollerRadius * (2 * cappedRatio)) / (cappedRatio + 1);
|
|
190
|
+
const innerRollerR = (rollerRadius * 2) / (cappedRatio + 1);
|
|
191
|
+
const roller = new THREE.CylinderGeometry(innerRollerR, outerRollerR, rollerLength, 16);
|
|
192
|
+
roller.rotateX(Math.PI / 2);
|
|
193
|
+
roller.rotateY(-radialAngle);
|
|
194
|
+
roller.translate(midX, rollerY, midZ);
|
|
195
|
+
rollerGeometries.push(roller);
|
|
196
|
+
}
|
|
197
|
+
if (rollerGeometries.length > 0) {
|
|
198
|
+
const rollerMesh = new THREE.Mesh(BufferGeometryUtils.mergeGeometries(rollerGeometries), rollerMaterial);
|
|
199
|
+
rollerMesh.castShadow = true;
|
|
200
|
+
this.object3d.add(rollerMesh);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
buildBelt(outerRx, outerRy, innerRx, innerRy, railW, startAngle, endAngle, segments, depth, frameH, value) {
|
|
204
|
+
const beltOx = outerRx - railW;
|
|
205
|
+
const beltOy = outerRy - railW;
|
|
206
|
+
const beltIx = innerRx + railW;
|
|
207
|
+
const beltIy = innerRy + railW;
|
|
208
|
+
const beltH = frameH * 0.15;
|
|
209
|
+
const fillColor = FILL_COLORS[value] ?? FILL_COLORS[0];
|
|
210
|
+
const shape = new THREE.Shape();
|
|
211
|
+
for (let i = 0; i <= segments; i++) {
|
|
212
|
+
const angle = startAngle + ((endAngle - startAngle) * i) / segments;
|
|
213
|
+
const sx = beltOx * Math.sin(angle);
|
|
214
|
+
const sy = beltOy * Math.cos(angle);
|
|
215
|
+
if (i === 0)
|
|
216
|
+
shape.moveTo(sx, sy);
|
|
217
|
+
else
|
|
218
|
+
shape.lineTo(sx, sy);
|
|
219
|
+
}
|
|
220
|
+
for (let i = segments; i >= 0; i--) {
|
|
221
|
+
const angle = startAngle + ((endAngle - startAngle) * i) / segments;
|
|
222
|
+
shape.lineTo(beltIx * Math.sin(angle), beltIy * Math.cos(angle));
|
|
223
|
+
}
|
|
224
|
+
shape.closePath();
|
|
225
|
+
const beltGeo = new THREE.ExtrudeGeometry(shape, { depth: beltH, bevelEnabled: false });
|
|
226
|
+
beltGeo.rotateX(-Math.PI / 2);
|
|
227
|
+
beltGeo.translate(0, depth / 2, 0);
|
|
228
|
+
const beltMesh = new THREE.Mesh(beltGeo, new THREE.MeshStandardMaterial({ color: fillColor, metalness: 0.0, roughness: 0.9 }));
|
|
229
|
+
beltMesh.castShadow = true;
|
|
230
|
+
this.object3d.add(beltMesh);
|
|
231
|
+
}
|
|
232
|
+
/** Control box + status light positioned at outer edge near start angle */
|
|
233
|
+
buildControlBox(outerRx, outerRy, innerRx, innerRy, startAngle, depth, frameH, railW, value) {
|
|
234
|
+
const beltWidth = outerRx - innerRx;
|
|
235
|
+
const boxW = Math.max(beltWidth * 0.12, 3);
|
|
236
|
+
const boxH = frameH * 0.6;
|
|
237
|
+
const boxD = Math.max(beltWidth * 0.15, 3);
|
|
238
|
+
// Position at outer edge near start angle
|
|
239
|
+
const midR = outerRx - railW - boxD / 2;
|
|
240
|
+
const boxX = midR * Math.sin(startAngle);
|
|
241
|
+
const boxZ = -midR * Math.cos(startAngle);
|
|
242
|
+
const railY = depth / 2 - frameH / 2;
|
|
243
|
+
const boxMaterial = new THREE.MeshStandardMaterial({
|
|
244
|
+
color: CONTROL_BOX_COLOR,
|
|
245
|
+
metalness: 0.6,
|
|
246
|
+
roughness: 0.4
|
|
247
|
+
});
|
|
248
|
+
const boxGeo = new THREE.BoxGeometry(boxW, boxH, boxD);
|
|
249
|
+
boxGeo.rotateY(-startAngle);
|
|
250
|
+
const boxMesh = new THREE.Mesh(boxGeo, boxMaterial);
|
|
251
|
+
boxMesh.position.set(boxX, railY - frameH / 2 - boxH / 2, boxZ);
|
|
252
|
+
boxMesh.castShadow = true;
|
|
253
|
+
this.object3d.add(boxMesh);
|
|
254
|
+
// Status light
|
|
255
|
+
const lightR = Math.min(boxW, boxD) * 0.25;
|
|
256
|
+
const lightH = lightR * 1.2;
|
|
257
|
+
const emissiveColor = STATUS_EMISSIVE[value] ?? STATUS_EMISSIVE[0];
|
|
258
|
+
const lightMaterial = new THREE.MeshStandardMaterial({
|
|
259
|
+
color: emissiveColor,
|
|
260
|
+
emissive: emissiveColor,
|
|
261
|
+
emissiveIntensity: value > 0 ? 1.5 : 0.2,
|
|
262
|
+
metalness: 0.0,
|
|
263
|
+
roughness: 0.3
|
|
264
|
+
});
|
|
265
|
+
const lightGeo = new THREE.CylinderGeometry(lightR, lightR * 0.8, lightH, 12);
|
|
266
|
+
const lightMesh = new THREE.Mesh(lightGeo, lightMaterial);
|
|
267
|
+
lightMesh.position.set(boxX, boxMesh.position.y + boxH / 2 + lightH / 2, boxZ);
|
|
268
|
+
this.object3d.add(lightMesh);
|
|
269
|
+
}
|
|
270
|
+
updateDimension() { }
|
|
271
|
+
onchange(after, before) {
|
|
272
|
+
if ('value' in after ||
|
|
273
|
+
'conveyorType' in after ||
|
|
274
|
+
'rollWidth' in after ||
|
|
275
|
+
'rx' in after ||
|
|
276
|
+
'ry' in after ||
|
|
277
|
+
'ratio' in after ||
|
|
278
|
+
'startAngle' in after ||
|
|
279
|
+
'endAngle' in after ||
|
|
280
|
+
'depth' in after) {
|
|
281
|
+
this.update();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
super.onchange(after, before);
|
|
285
|
+
}
|
|
286
|
+
updateAlpha() { }
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=conveyor-join-3d.js.map
|