@operato/scene-urdf 10.0.0-beta.18 → 10.0.0-beta.19

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.
@@ -13,9 +13,12 @@
13
13
  */
14
14
  import * as THREE from 'three';
15
15
  import URDFLoader from 'urdf-loader';
16
+ import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
17
+ import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js';
16
18
  import { warn } from '@hatiolab/things-scene';
17
19
  import { RealObjectExternalModel } from '@hatiolab/things-scene';
18
20
  import { RealObjectGLTF } from '@hatiolab/things-scene';
21
+ import { createJointController } from './joint-controller.js';
19
22
  const X_NEG_PI_HALF = -Math.PI / 2;
20
23
  export class RealObjectURDF extends RealObjectExternalModel {
21
24
  get _formatLabel() { return 'urdf'; }
@@ -26,25 +29,62 @@ export class RealObjectURDF extends RealObjectExternalModel {
26
29
  static _topViewCache = new Map();
27
30
  /**
28
31
  * URDF를 로드한다 (동일 URL 캐시).
29
- * mesh 로더는 STL/DAE(내장) + GLB(확장)를 지원한다.
32
+ * mesh 로더는 STL/DAE(내장) + OBJ(MTL 연동) + GLB(확장)를 지원한다.
30
33
  *
31
34
  * URDFLoader.loadAsync는 URDF XML 파싱 직후 resolve하지만 mesh(STL/DAE/GLB)는
32
35
  * 비동기로 뒤늦게 attach된다. robot.clone() 시점에 mesh가 아직 없으면 클론된
33
36
  * 트리엔 visual이 비게 되고 원본에만 mesh가 붙는다. 전용 LoadingManager의
34
37
  * onLoad를 사용해 URDF + 모든 mesh가 완료된 후 resolve한다.
38
+ *
39
+ * `packages` 옵션을 통해 ROS `package://<pkg>/<rel>` prefix 해석을 설정할 수
40
+ * 있다 (예: `{ a1_description: 'https://.../robots/a1_description' }`).
35
41
  */
36
- static loadURDF(url) {
37
- let cached = RealObjectURDF._urdfCache.get(url);
42
+ static loadURDF(url, packages) {
43
+ // 동일 URL이라도 packages 설정이 다르면 다른 캐시 엔트리.
44
+ const cacheKey = packages && Object.keys(packages).length > 0
45
+ ? `${url}|${JSON.stringify(packages)}`
46
+ : url;
47
+ let cached = RealObjectURDF._urdfCache.get(cacheKey);
38
48
  if (!cached) {
39
49
  cached = new Promise((resolve, reject) => {
40
50
  const manager = new THREE.LoadingManager();
41
51
  const loader = new URDFLoader(manager);
52
+ if (packages) {
53
+ loader.packages = packages;
54
+ }
42
55
  loader.loadMeshCb = ((path, mgr, done) => {
43
56
  if (/\.(glb|gltf)$/i.test(path)) {
44
57
  // GLB는 별도 시스템 — manager 추적에서 벗어나므로 명시적으로 track.
45
58
  mgr.itemStart(path);
46
59
  RealObjectGLTF.loadGLTF(path).then(gltf => { done(gltf.scene.clone()); mgr.itemEnd(path); }, err => { done(null, err); mgr.itemError(path); mgr.itemEnd(path); });
47
60
  }
61
+ else if (/\.obj$/i.test(path)) {
62
+ // OBJ는 urdf-loader 내장 defaultMeshLoader가 지원하지 않음.
63
+ // 연관된 .mtl(재질) 파일이 있으면 MTLLoader로 먼저 로드하여 색상/텍스처
64
+ // 적용, 없으면 OBJLoader만 폴백.
65
+ //
66
+ // LoadingManager 정합성: 외부 path로만 itemStart/itemEnd를 1회씩
67
+ // 기록한다. 내부 MTLLoader/OBJLoader는 manager 주입 없이 생성하여
68
+ // 서브-아이템으로 카운트되지 않게 함 — 그렇지 않으면 MTL 404 직후
69
+ // 일시적으로 pending=0이 되어 manager.onLoad가 조기 발화하고
70
+ // 최종 로봇이 빈 상태로 resolve되는 레이스가 발생.
71
+ mgr.itemStart(path);
72
+ const finishOk = (mesh) => { done(mesh); mgr.itemEnd(path); };
73
+ const finishErr = (err) => { done(null, err); mgr.itemError(path); mgr.itemEnd(path); };
74
+ const lastSlash = path.lastIndexOf('/');
75
+ const baseUrl = lastSlash >= 0 ? path.substring(0, lastSlash + 1) : '';
76
+ const mtlName = path.substring(lastSlash + 1).replace(/\.obj$/i, '.mtl');
77
+ const loadObjOnly = () => {
78
+ new OBJLoader().load(path, group => finishOk(group), undefined, err => finishErr(err));
79
+ };
80
+ new MTLLoader().setResourcePath(baseUrl).setPath(baseUrl).load(mtlName, materials => {
81
+ materials.preload();
82
+ new OBJLoader().setMaterials(materials).load(path, group => finishOk(group), undefined, err => finishErr(err));
83
+ }, undefined, () => {
84
+ // MTL 로드 실패 시 OBJ만 로드 (정상 시나리오 — MTL 없는 OBJ도 있음)
85
+ loadObjOnly();
86
+ });
87
+ }
48
88
  else {
49
89
  // STL/DAE는 urdf-loader 내장 디폴트로 폴백 (manager 자동 추적)
50
90
  ;
@@ -65,10 +105,10 @@ export class RealObjectURDF extends RealObjectExternalModel {
65
105
  };
66
106
  loader.load(url, robot => { robotResult = robot; }, undefined, err => reject(err));
67
107
  }).catch(err => {
68
- RealObjectURDF._urdfCache.delete(url);
108
+ RealObjectURDF._urdfCache.delete(cacheKey);
69
109
  throw err;
70
110
  });
71
- RealObjectURDF._urdfCache.set(url, cached);
111
+ RealObjectURDF._urdfCache.set(cacheKey, cached);
72
112
  }
73
113
  return cached;
74
114
  }
@@ -90,6 +130,13 @@ export class RealObjectURDF extends RealObjectExternalModel {
90
130
  _jointMeta = new Map();
91
131
  // 원본 조인트 값 — reset을 위해 보존
92
132
  _jointOriginals = new Map();
133
+ // 자동 애니메이션 (sine 모드) 상태
134
+ _animRaf;
135
+ _animStartTime = 0;
136
+ // Joint controller (physics mode) 상태
137
+ _controller;
138
+ _ctrlRaf;
139
+ _ctrlLastTime = 0;
93
140
  get robot() {
94
141
  return this._robot;
95
142
  }
@@ -101,7 +148,8 @@ export class RealObjectURDF extends RealObjectExternalModel {
101
148
  return Array.from(this._jointIndex.keys());
102
149
  }
103
150
  _loadExternal(url) {
104
- return RealObjectURDF.loadURDF(url);
151
+ const packages = this.component.state.packages;
152
+ return RealObjectURDF.loadURDF(url, packages);
105
153
  }
106
154
  _onLoaded(robot) {
107
155
  if (this.component.state.loadError) {
@@ -143,10 +191,48 @@ export class RealObjectURDF extends RealObjectExternalModel {
143
191
  const box = new THREE.Box3().setFromObject(this.pivot);
144
192
  this._objectSize = box.getSize(new THREE.Vector3());
145
193
  this.updateDimension();
194
+ // 자동 placement 정렬: URDF의 base_link 원점이 로봇 기하의 최저점이 아닐 때
195
+ // (Nao/minitaur/quadrotor는 torso/중심 기준) 로봇 일부가 컴포넌트 범위를
196
+ // 벗어나는 것을 방지. 씬의 placement 모드에 따라 정렬 기준이 다르다.
197
+ // - floor: 로봇 최저점 → 컴포넌트 바닥면
198
+ // - inverted: 로봇 최상점 → 컴포넌트 천장면 (매달림)
199
+ // - space: 정렬 없음 (중심 원점 유지)
200
+ // state.floorAlign === false 로 비활성화 가능.
201
+ if (this.component.state.floorAlign !== false) {
202
+ const placement = this.scenePlacement;
203
+ if (placement !== 'space') {
204
+ this.components3D.updateMatrixWorld(true);
205
+ const worldBox = new THREE.Box3().setFromObject(this.pivot);
206
+ if (isFinite(worldBox.min.y) && isFinite(worldBox.max.y)) {
207
+ if (placement === 'inverted') {
208
+ const localMax = this.components3D.worldToLocal(worldBox.max.clone());
209
+ if (localMax.y > 0) {
210
+ this.pivot.position.y -= localMax.y;
211
+ }
212
+ }
213
+ else {
214
+ // floor (기본)
215
+ const localMin = this.components3D.worldToLocal(worldBox.min.clone());
216
+ if (localMin.y < 0) {
217
+ this.pivot.position.y -= localMin.y;
218
+ }
219
+ }
220
+ }
221
+ }
222
+ }
223
+ // 컨트롤러 초기화 (state.physics 설정된 경우)
224
+ const physicsMode = this.component.state.physics;
225
+ if (physicsMode && physicsMode !== 'none') {
226
+ this._setupController(physicsMode);
227
+ }
146
228
  // 초기 joint 상태 적용
147
229
  const jointStates = this.component.state.joints;
148
230
  if (jointStates) {
149
- this._applyJointStates(jointStates);
231
+ this._dispatchJointStates(jointStates);
232
+ }
233
+ // 자동 애니메이션 시작 (state.autoAnimate === 'sine'인 경우)
234
+ if (this.component.state.autoAnimate === 'sine') {
235
+ this._startAutoAnimate();
150
236
  }
151
237
  // 탑뷰 스냅샷
152
238
  const source = this.component.state.src;
@@ -195,7 +281,78 @@ export class RealObjectURDF extends RealObjectExternalModel {
195
281
  }
196
282
  }
197
283
  /**
198
- * state.joints 맵을 순회하며 조인트 값을 적용한다.
284
+ * state.joints 변경을 적절한 경로로 디스패치한다.
285
+ * - controller가 활성이면 setTargets (controller가 스무딩/물리 적용)
286
+ * - 없으면 즉시 적용 (direct 모드, 기본)
287
+ */
288
+ _dispatchJointStates(states) {
289
+ if (this._controller) {
290
+ const targets = {};
291
+ for (const [name, raw] of Object.entries(states)) {
292
+ const v = typeof raw === 'number' ? raw : Number(raw?.value);
293
+ if (Number.isFinite(v))
294
+ targets[name] = v;
295
+ }
296
+ this._controller.setTargets(targets);
297
+ return;
298
+ }
299
+ this._applyJointStates(states);
300
+ }
301
+ /**
302
+ * Controller를 인스턴스화하고 setup + tick 루프 시작.
303
+ * 이미 활성 중이면 먼저 해제.
304
+ */
305
+ _setupController(mode) {
306
+ this._disposeController();
307
+ const ctrl = createJointController(mode);
308
+ if (!ctrl) {
309
+ warn(`URDFObject: unknown physics mode '${mode}'`);
310
+ return;
311
+ }
312
+ if (!this._robot)
313
+ return;
314
+ ctrl.setup({ robot: this._robot, joints: this._jointIndex, jointMeta: this._jointMeta });
315
+ this._controller = ctrl;
316
+ // 현재 state.joints를 초기 타겟으로 설정
317
+ const s = this.component.state.joints;
318
+ if (s) {
319
+ const targets = {};
320
+ for (const [name, raw] of Object.entries(s)) {
321
+ const v = typeof raw === 'number' ? raw : Number(raw?.value);
322
+ if (Number.isFinite(v))
323
+ targets[name] = v;
324
+ }
325
+ ctrl.setTargets(targets);
326
+ }
327
+ this._ctrlLastTime = performance.now();
328
+ const tick = () => {
329
+ if (!this._controller) {
330
+ this._ctrlRaf = undefined;
331
+ return;
332
+ }
333
+ const now = performance.now();
334
+ const dt = (now - this._ctrlLastTime) / 1000;
335
+ this._ctrlLastTime = now;
336
+ const changed = this._controller.tick(dt);
337
+ if (changed) {
338
+ this.component?.invalidate?.();
339
+ }
340
+ this._ctrlRaf = requestAnimationFrame(tick);
341
+ };
342
+ this._ctrlRaf = requestAnimationFrame(tick);
343
+ }
344
+ _disposeController() {
345
+ if (this._ctrlRaf != null) {
346
+ cancelAnimationFrame(this._ctrlRaf);
347
+ this._ctrlRaf = undefined;
348
+ }
349
+ if (this._controller) {
350
+ this._controller.dispose();
351
+ this._controller = undefined;
352
+ }
353
+ }
354
+ /**
355
+ * state.joints 맵을 순회하며 각 조인트 값을 적용한다 (direct 모드).
199
356
  * 값은 숫자 또는 { value: number } 형식 모두 허용 (유연성).
200
357
  *
201
358
  * joint transform 변경 후 component.invalidate()로 재렌더를 요청한다.
@@ -220,6 +377,57 @@ export class RealObjectURDF extends RealObjectExternalModel {
220
377
  this.component?.invalidate?.();
221
378
  }
222
379
  }
380
+ /**
381
+ * 자동 sine 애니메이션 시작.
382
+ * 각 non-fixed 조인트를 limit 범위 내에서 sine 스윙. 조인트 순서에 따라
383
+ * frequency와 phase를 약간씩 다르게 해 동기화를 피하고 유기적 움직임을 만든다.
384
+ *
385
+ * state.joints를 건드리지 않고 joint.setJointValue만 직접 호출 — state 변경
386
+ * cascade를 피해 성능을 유지하고, 슬라이더가 있는 경우에도 state와 시각이
387
+ * 분리되어 표시됨 (활성 시 state는 의미 없음).
388
+ */
389
+ _startAutoAnimate() {
390
+ if (this._animRaf != null)
391
+ return;
392
+ if (!this._robot || this._jointMeta.size === 0)
393
+ return;
394
+ this._animStartTime = performance.now();
395
+ const TWO_PI = Math.PI * 2;
396
+ const tick = () => {
397
+ if (!this._robot) {
398
+ this._animRaf = undefined;
399
+ return;
400
+ }
401
+ const t = (performance.now() - this._animStartTime) / 1000;
402
+ let i = 0;
403
+ let changed = false;
404
+ for (const [name, meta] of this._jointMeta) {
405
+ const joint = this._jointIndex.get(name);
406
+ if (!joint)
407
+ continue;
408
+ const mid = (meta.limit.upper + meta.limit.lower) / 2;
409
+ const amp = (meta.limit.upper - meta.limit.lower) / 2;
410
+ // 조인트별로 frequency와 phase 약간씩 다르게 (0.15 ~ 0.4 Hz 범위)
411
+ const freq = 0.15 + (i % 6) * 0.05;
412
+ const phase = i * 0.73;
413
+ const value = mid + amp * Math.sin(t * freq * TWO_PI + phase);
414
+ joint.setJointValue(value);
415
+ changed = true;
416
+ i++;
417
+ }
418
+ if (changed) {
419
+ this.component?.invalidate?.();
420
+ }
421
+ this._animRaf = requestAnimationFrame(tick);
422
+ };
423
+ this._animRaf = requestAnimationFrame(tick);
424
+ }
425
+ _stopAutoAnimate() {
426
+ if (this._animRaf != null) {
427
+ cancelAnimationFrame(this._animRaf);
428
+ this._animRaf = undefined;
429
+ }
430
+ }
223
431
  /**
224
432
  * 모든 조인트를 원본 값으로 복원한다.
225
433
  */
@@ -235,6 +443,8 @@ export class RealObjectURDF extends RealObjectExternalModel {
235
443
  }
236
444
  // --- 라이프사이클 ---
237
445
  clear() {
446
+ this._stopAutoAnimate();
447
+ this._disposeController();
238
448
  this._jointIndex.clear();
239
449
  this._jointMeta.clear();
240
450
  this._jointOriginals.clear();
@@ -269,14 +479,47 @@ export class RealObjectURDF extends RealObjectExternalModel {
269
479
  return;
270
480
  }
271
481
  if ('joints' in after) {
272
- this._resetAllJoints();
482
+ // 사용자가 joint를 직접 조작하면 자동 애니메이션이 이를 즉시 덮어써
483
+ // 의도를 상쇄하므로, 자동 애니메이션을 우선 중지한다.
484
+ if (this._animRaf != null) {
485
+ this._stopAutoAnimate();
486
+ this._component.setState({ autoAnimate: 'none' });
487
+ }
273
488
  const jointStates = after.joints;
274
- if (jointStates && Object.keys(jointStates).length > 0) {
275
- this._applyJointStates(jointStates);
489
+ if (this._controller) {
490
+ // controller 모드: 타겟만 갱신. 이전 값 리셋은 controller가 연속 감쇠로
491
+ // 알아서 수렴하므로 불필요.
492
+ if (jointStates && Object.keys(jointStates).length > 0) {
493
+ this._dispatchJointStates(jointStates);
494
+ }
495
+ }
496
+ else {
497
+ // direct 모드: 전체 reset 후 명시된 값만 재적용 (sparse state 지원).
498
+ this._resetAllJoints();
499
+ if (jointStates && Object.keys(jointStates).length > 0) {
500
+ this._applyJointStates(jointStates);
501
+ }
502
+ }
503
+ }
504
+ if ('autoAnimate' in after) {
505
+ if (after.autoAnimate === 'sine') {
506
+ this._startAutoAnimate();
507
+ }
508
+ else {
509
+ this._stopAutoAnimate();
510
+ }
511
+ }
512
+ if ('physics' in after) {
513
+ const mode = after.physics;
514
+ if (mode && mode !== 'none') {
515
+ this._setupController(mode);
516
+ }
517
+ else {
518
+ this._disposeController();
276
519
  }
277
520
  }
278
- if ('upAxis' in after || 'unitScale' in after) {
279
- // 좌표/스케일 변경은 pivot 재구성이 필요하므로 전체 재빌드
521
+ if ('upAxis' in after || 'unitScale' in after || 'packages' in after) {
522
+ // 좌표/스케일/패키지 매핑 변경은 pivot 재구성이 필요하므로 전체 재빌드
280
523
  this.build();
281
524
  }
282
525
  }
@@ -1 +1 @@
1
- {"version":3,"file":"real-object-urdf.js","sourceRoot":"","sources":["../src/real-object-urdf.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * URDF (Unified Robot Description Format) 지원 RealObject.\n * RealObjectGLTF와 동일한 캐시/레이스/placeholder 패턴을 따른다.\n *\n * 차이점: GLTF는 노드/애니메이션이 주된 바인딩 대상이지만, URDF는 \"joint\"가\n * 1급 시민이다. state.joints에 이름→라디안(또는 prismatic의 경우 거리) 맵을\n * 두어 외부 센서값 등을 연결할 수 있도록 한다.\n *\n * 좌표계: URDF는 Z-up, things-scene의 3D는 Y-up. 로드 후 루트 그룹에 1회만\n * -PI/2 X축 회전을 적용하여 변환한다. 내부 조인트 축은 원본 그대로 유지.\n */\n\nimport * as THREE from 'three'\nimport URDFLoader from 'urdf-loader'\nimport type { URDFRobot, URDFJoint } from 'urdf-loader'\n\nimport { warn } from '@hatiolab/things-scene'\nimport type { Properties } from '@hatiolab/things-scene'\n\nimport { RealObjectExternalModel } from '@hatiolab/things-scene'\nimport { RealObjectGLTF } from '@hatiolab/things-scene'\n\nexport interface URDFJointState {\n /** revolute/continuous: 라디안. prismatic: 거리(URDF 단위). */\n value?: number\n}\n\nexport interface URDFJointMeta {\n name: string\n type: 'fixed' | 'continuous' | 'revolute' | 'planar' | 'prismatic' | 'floating'\n axis: { x: number; y: number; z: number }\n limit: { lower: number; upper: number; effort: number; velocity: number }\n}\n\nconst X_NEG_PI_HALF = -Math.PI / 2\n\nexport class RealObjectURDF extends RealObjectExternalModel<URDFRobot> {\n protected get _formatLabel() { return 'urdf' }\n protected get _placeholderColor() { return 0x1976d2 }\n // URDF 파싱 결과 캐시 — 동일 URL 재파싱 방지. clone()으로 복제 사용.\n private static _urdfCache = new Map<string, Promise<URDFRobot>>()\n\n // 탑뷰 스냅샷 캐시 (2D 렌더 폴백)\n private static _topViewCache = new Map<string, HTMLCanvasElement>()\n\n /**\n * URDF를 로드한다 (동일 URL 캐시).\n * mesh 로더는 STL/DAE(내장) + GLB(확장)를 지원한다.\n *\n * URDFLoader.loadAsync는 URDF XML 파싱 직후 resolve하지만 mesh(STL/DAE/GLB)는\n * 비동기로 뒤늦게 attach된다. robot.clone() 시점에 mesh가 아직 없으면 클론된\n * 트리엔 visual이 비게 되고 원본에만 mesh가 붙는다. 전용 LoadingManager의\n * onLoad를 사용해 URDF + 모든 mesh가 완료된 후 resolve한다.\n */\n static loadURDF(url: string): Promise<URDFRobot> {\n let cached = RealObjectURDF._urdfCache.get(url)\n if (!cached) {\n cached = new Promise<URDFRobot>((resolve, reject) => {\n const manager = new THREE.LoadingManager()\n const loader = new URDFLoader(manager)\n\n loader.loadMeshCb = ((path: string, mgr: THREE.LoadingManager, done: (mesh: any, err?: Error) => void) => {\n if (/\\.(glb|gltf)$/i.test(path)) {\n // GLB는 별도 시스템 — manager 추적에서 벗어나므로 명시적으로 track.\n mgr.itemStart(path)\n RealObjectGLTF.loadGLTF(path).then(\n gltf => { done(gltf.scene.clone()); mgr.itemEnd(path) },\n err => { done(null, err); mgr.itemError(path); mgr.itemEnd(path) }\n )\n } else {\n // STL/DAE는 urdf-loader 내장 디폴트로 폴백 (manager 자동 추적)\n ;(loader as any).defaultMeshLoader(path, mgr, done)\n }\n }) as any\n\n let robotResult: URDFRobot | undefined\n\n manager.onLoad = () => {\n if (robotResult) resolve(robotResult)\n else reject(new Error('URDF load completed but no robot result'))\n }\n manager.onError = (failedUrl: string) => {\n // 개별 에러는 로그로만 남김 — 전체 로드는 그래도 완료되도록\n // (일부 mesh 실패 시 나머지라도 보이게)\n warn('URDFLoader: resource load error', failedUrl)\n }\n\n loader.load(\n url,\n robot => { robotResult = robot },\n undefined,\n err => reject(err)\n )\n }).catch(err => {\n RealObjectURDF._urdfCache.delete(url)\n throw err\n })\n RealObjectURDF._urdfCache.set(url, cached)\n }\n return cached\n }\n\n /** 모든 캐시 비움 (보드 전환 시 호출) */\n static flushCache() {\n RealObjectURDF._urdfCache.clear()\n RealObjectURDF._topViewCache.clear()\n }\n\n static getTopViewCache(source: string): HTMLCanvasElement | undefined {\n return RealObjectURDF._topViewCache.get(source)\n }\n\n static setTopViewCache(source: string, canvas: HTMLCanvasElement) {\n RealObjectURDF._topViewCache.set(source, canvas)\n }\n\n // --- 인스턴스 상태 ---\n\n private _robot?: URDFRobot\n\n // 조인트 인덱스 + 메타 (로드 후 외부에서 구독 가능)\n private _jointIndex = new Map<string, URDFJoint>()\n private _jointMeta = new Map<string, URDFJointMeta>()\n\n // 원본 조인트 값 — reset을 위해 보존\n private _jointOriginals = new Map<string, number>()\n\n get robot(): URDFRobot | undefined {\n return this._robot\n }\n\n /** 로드된 조인트 메타 리스트 (property panel 등이 참조) */\n get jointMeta(): URDFJointMeta[] {\n return Array.from(this._jointMeta.values())\n }\n\n get jointNames(): string[] {\n return Array.from(this._jointIndex.keys())\n }\n\n protected _loadExternal(url: string): Promise<URDFRobot> {\n return RealObjectURDF.loadURDF(url)\n }\n\n protected _onLoaded(robot: URDFRobot) {\n if (this.component.state.loadError) {\n this.component.setState({ loadError: undefined })\n }\n\n // 캐시의 robot은 공유 참조이므로 clone하여 독립 인스턴스 구성.\n // urdf-loader의 URDFRobot는 Object3D를 상속하므로 Object3D.clone()이 안전하게 동작.\n const cloned = robot.clone() as URDFRobot\n\n // 섀도우 캐스팅\n cloned.traverse(child => {\n if ((child as THREE.Mesh).isMesh) {\n child.castShadow = true\n }\n })\n\n // clear()는 object3d.userData를 리셋하므로 pending 후 재설정.\n this.clear()\n this.object3d.userData.context = this\n\n this._robot = cloned\n\n // 좌표계 변환: URDF(Z-up) → three(Y-up).\n // 사용자가 state.upAxis = 'y' 로 지정하면 변환 생략.\n this.pivot = new THREE.Object3D()\n const upAxis = this.component.state.upAxis\n if (upAxis !== 'y') {\n this.pivot.rotation.x = X_NEG_PI_HALF\n }\n\n // URDF는 기본 meter 단위. state.unitScale로 에디터 단위로 환산.\n const unitScale = this._resolveUnitScale()\n if (unitScale !== 1) {\n this.pivot.scale.setScalar(unitScale)\n }\n\n this.pivot.add(cloned)\n // URDF의 base_link는 관행상 z=0이 바닥(floor origin). things-scene는 center\n // origin이므로, components3D 그룹(= -geometricOffsetY만큼 shift된 바닥 기준\n // subgroup)에 pivot을 넣어 로봇이 컴포넌트 바닥면에 서도록 한다.\n this.components3D.add(this.pivot)\n\n // 조인트 인덱스/메타 구축\n this._buildJointIndex(cloned)\n\n // bounding box 계산 후 updateDimension으로 크기 반영\n const box = new THREE.Box3().setFromObject(this.pivot)\n this._objectSize = box.getSize(new THREE.Vector3())\n\n this.updateDimension()\n\n // 초기 joint 상태 적용\n const jointStates = this.component.state.joints as Record<string, URDFJointState | number> | undefined\n if (jointStates) {\n this._applyJointStates(jointStates)\n }\n\n // 탑뷰 스냅샷\n const source = this.component.state.src as string\n if (source && !RealObjectURDF._topViewCache.has(source)) {\n try {\n const canvas = RealObjectGLTF.renderTopView(this.pivot)\n RealObjectURDF._topViewCache.set(source, canvas)\n ;(this.component as any)._topViewSnapshot = canvas\n } catch {\n // 스냅샷 실패는 치명적이지 않음\n }\n } else if (source) {\n ;(this.component as any)._topViewSnapshot = RealObjectURDF._topViewCache.get(source)\n }\n }\n\n private _resolveUnitScale(): number {\n const s = this.component.state.unitScale\n if (typeof s === 'number' && s > 0) return s\n // 기본: URDF meter → 에디터 mm 스케일 1000\n return 1000\n }\n\n /**\n * 조인트 트리를 순회하며 인덱스와 메타를 구축한다.\n * fixed 조인트는 조작 대상이 아니므로 메타에서 제외하되 인덱스에는 남긴다.\n */\n private _buildJointIndex(robot: URDFRobot) {\n this._jointIndex.clear()\n this._jointMeta.clear()\n this._jointOriginals.clear()\n\n const joints = robot.joints || {}\n for (const [name, joint] of Object.entries(joints)) {\n this._jointIndex.set(name, joint)\n this._jointOriginals.set(name, joint.angle || 0)\n\n if (joint.jointType === 'fixed') continue\n\n this._jointMeta.set(name, {\n name,\n type: joint.jointType,\n axis: { x: joint.axis.x, y: joint.axis.y, z: joint.axis.z },\n limit: { ...joint.limit }\n })\n }\n }\n\n /**\n * state.joints 맵을 순회하며 각 조인트 값을 적용한다.\n * 값은 숫자 또는 { value: number } 형식 모두 허용 (유연성).\n *\n * joint transform 변경 후 component.invalidate()로 재렌더를 요청한다.\n * 표준 경로: invalidate → Layer.throttle_render → rAF __draw__ → trigger('redraw')\n * → ThreeCapability.animate → renderThreeD.\n */\n private _applyJointStates(states: Record<string, URDFJointState | number>) {\n if (!this._robot) return\n\n let changed = false\n for (const [name, raw] of Object.entries(states)) {\n const joint = this._jointIndex.get(name)\n if (!joint || joint.jointType === 'fixed') continue\n\n const value = typeof raw === 'number' ? raw : raw?.value\n if (typeof value !== 'number' || !isFinite(value)) continue\n\n joint.setJointValue(value)\n changed = true\n }\n\n if (changed) {\n this.component?.invalidate?.()\n }\n }\n\n /**\n * 모든 조인트를 원본 값으로 복원한다.\n */\n private _resetAllJoints() {\n if (!this._robot) return\n for (const [name, orig] of this._jointOriginals) {\n const joint = this._jointIndex.get(name)\n if (joint && joint.jointType !== 'fixed') {\n joint.setJointValue(orig)\n }\n }\n }\n\n // --- 라이프사이클 ---\n\n clear() {\n this._jointIndex.clear()\n this._jointMeta.clear()\n this._jointOriginals.clear()\n this._robot = undefined\n\n return super.clear()\n }\n\n updateDimension() {\n if (!this.pivot) return\n\n const { width = 1, height = 1, depth = 1 } = this.component.state\n const { x = 1, y = 1, z = 1 } = this._objectSize || {}\n\n // URDF는 관절 기반 로봇의 비율을 유지해야 하므로 uniform scale을 적용한다.\n // 세 축 각각의 비율 중 가장 작은 값(best-fit)을 사용해 로봇이 컴포넌트의\n // bounding box 안에 왜곡 없이 맞춰지도록 한다.\n const unit = this._resolveUnitScale()\n const baseX = x || 1\n const baseY = y || 1\n const baseZ = z || 1\n const upAxis = this.component.state.upAxis\n const rotated = upAxis !== 'y'\n\n const rX = width / baseX\n const rY = (rotated ? depth : height) / baseY\n const rZ = (rotated ? height : depth) / baseZ\n const ratio = Math.min(rX, rY, rZ)\n\n this.pivot.scale.setScalar(ratio * unit)\n\n this.component.invalidate()\n }\n\n onchange(after: Properties, before: Properties) {\n super.onchange(after, before)\n\n if ('src' in after) {\n this.updateSource()\n return\n }\n\n if ('joints' in after) {\n this._resetAllJoints()\n const jointStates = after.joints as Record<string, URDFJointState | number> | undefined\n if (jointStates && Object.keys(jointStates).length > 0) {\n this._applyJointStates(jointStates)\n }\n }\n\n if ('upAxis' in after || 'unitScale' in after) {\n // 좌표/스케일 변경은 pivot 재구성이 필요하므로 전체 재빌드\n this.build()\n }\n }\n}\n"]}
1
+ {"version":3,"file":"real-object-urdf.js","sourceRoot":"","sources":["../src/real-object-urdf.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * URDF (Unified Robot Description Format) 지원 RealObject.\n * RealObjectGLTF와 동일한 캐시/레이스/placeholder 패턴을 따른다.\n *\n * 차이점: GLTF는 노드/애니메이션이 주된 바인딩 대상이지만, URDF는 \"joint\"가\n * 1급 시민이다. state.joints에 이름→라디안(또는 prismatic의 경우 거리) 맵을\n * 두어 외부 센서값 등을 연결할 수 있도록 한다.\n *\n * 좌표계: URDF는 Z-up, things-scene의 3D는 Y-up. 로드 후 루트 그룹에 1회만\n * -PI/2 X축 회전을 적용하여 변환한다. 내부 조인트 축은 원본 그대로 유지.\n */\n\nimport * as THREE from 'three'\nimport URDFLoader from 'urdf-loader'\nimport type { URDFRobot, URDFJoint } from 'urdf-loader'\nimport { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'\nimport { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader.js'\n\nimport { warn } from '@hatiolab/things-scene'\nimport type { Properties } from '@hatiolab/things-scene'\n\nimport { RealObjectExternalModel } from '@hatiolab/things-scene'\nimport { RealObjectGLTF } from '@hatiolab/things-scene'\n\nimport { createJointController, type JointController } from './joint-controller.js'\n\nexport interface URDFJointState {\n /** revolute/continuous: 라디안. prismatic: 거리(URDF 단위). */\n value?: number\n}\n\nexport interface URDFJointMeta {\n name: string\n type: 'fixed' | 'continuous' | 'revolute' | 'planar' | 'prismatic' | 'floating'\n axis: { x: number; y: number; z: number }\n limit: { lower: number; upper: number; effort: number; velocity: number }\n}\n\nconst X_NEG_PI_HALF = -Math.PI / 2\n\nexport class RealObjectURDF extends RealObjectExternalModel<URDFRobot> {\n protected get _formatLabel() { return 'urdf' }\n protected get _placeholderColor() { return 0x1976d2 }\n // URDF 파싱 결과 캐시 — 동일 URL 재파싱 방지. clone()으로 복제 사용.\n private static _urdfCache = new Map<string, Promise<URDFRobot>>()\n\n // 탑뷰 스냅샷 캐시 (2D 렌더 폴백)\n private static _topViewCache = new Map<string, HTMLCanvasElement>()\n\n /**\n * URDF를 로드한다 (동일 URL 캐시).\n * mesh 로더는 STL/DAE(내장) + OBJ(MTL 연동) + GLB(확장)를 지원한다.\n *\n * URDFLoader.loadAsync는 URDF XML 파싱 직후 resolve하지만 mesh(STL/DAE/GLB)는\n * 비동기로 뒤늦게 attach된다. robot.clone() 시점에 mesh가 아직 없으면 클론된\n * 트리엔 visual이 비게 되고 원본에만 mesh가 붙는다. 전용 LoadingManager의\n * onLoad를 사용해 URDF + 모든 mesh가 완료된 후 resolve한다.\n *\n * `packages` 옵션을 통해 ROS `package://<pkg>/<rel>` prefix 해석을 설정할 수\n * 있다 (예: `{ a1_description: 'https://.../robots/a1_description' }`).\n */\n static loadURDF(url: string, packages?: Record<string, string>): Promise<URDFRobot> {\n // 동일 URL이라도 packages 설정이 다르면 다른 캐시 엔트리.\n const cacheKey = packages && Object.keys(packages).length > 0\n ? `${url}|${JSON.stringify(packages)}`\n : url\n let cached = RealObjectURDF._urdfCache.get(cacheKey)\n if (!cached) {\n cached = new Promise<URDFRobot>((resolve, reject) => {\n const manager = new THREE.LoadingManager()\n const loader = new URDFLoader(manager)\n if (packages) {\n loader.packages = packages\n }\n\n loader.loadMeshCb = ((path: string, mgr: THREE.LoadingManager, done: (mesh: any, err?: Error) => void) => {\n if (/\\.(glb|gltf)$/i.test(path)) {\n // GLB는 별도 시스템 — manager 추적에서 벗어나므로 명시적으로 track.\n mgr.itemStart(path)\n RealObjectGLTF.loadGLTF(path).then(\n gltf => { done(gltf.scene.clone()); mgr.itemEnd(path) },\n err => { done(null, err); mgr.itemError(path); mgr.itemEnd(path) }\n )\n } else if (/\\.obj$/i.test(path)) {\n // OBJ는 urdf-loader 내장 defaultMeshLoader가 지원하지 않음.\n // 연관된 .mtl(재질) 파일이 있으면 MTLLoader로 먼저 로드하여 색상/텍스처\n // 적용, 없으면 OBJLoader만 폴백.\n //\n // LoadingManager 정합성: 외부 path로만 itemStart/itemEnd를 1회씩\n // 기록한다. 내부 MTLLoader/OBJLoader는 manager 주입 없이 생성하여\n // 서브-아이템으로 카운트되지 않게 함 — 그렇지 않으면 MTL 404 직후\n // 일시적으로 pending=0이 되어 manager.onLoad가 조기 발화하고\n // 최종 로봇이 빈 상태로 resolve되는 레이스가 발생.\n mgr.itemStart(path)\n const finishOk = (mesh: any) => { done(mesh); mgr.itemEnd(path) }\n const finishErr = (err: Error) => { done(null, err); mgr.itemError(path); mgr.itemEnd(path) }\n\n const lastSlash = path.lastIndexOf('/')\n const baseUrl = lastSlash >= 0 ? path.substring(0, lastSlash + 1) : ''\n const mtlName = path.substring(lastSlash + 1).replace(/\\.obj$/i, '.mtl')\n\n const loadObjOnly = () => {\n new OBJLoader().load(\n path,\n group => finishOk(group),\n undefined,\n err => finishErr(err as Error)\n )\n }\n new MTLLoader().setResourcePath(baseUrl).setPath(baseUrl).load(\n mtlName,\n materials => {\n materials.preload()\n new OBJLoader().setMaterials(materials).load(\n path,\n group => finishOk(group),\n undefined,\n err => finishErr(err as Error)\n )\n },\n undefined,\n () => {\n // MTL 로드 실패 시 OBJ만 로드 (정상 시나리오 — MTL 없는 OBJ도 있음)\n loadObjOnly()\n }\n )\n } else {\n // STL/DAE는 urdf-loader 내장 디폴트로 폴백 (manager 자동 추적)\n ;(loader as any).defaultMeshLoader(path, mgr, done)\n }\n }) as any\n\n let robotResult: URDFRobot | undefined\n\n manager.onLoad = () => {\n if (robotResult) resolve(robotResult)\n else reject(new Error('URDF load completed but no robot result'))\n }\n manager.onError = (failedUrl: string) => {\n // 개별 에러는 로그로만 남김 — 전체 로드는 그래도 완료되도록\n // (일부 mesh 실패 시 나머지라도 보이게)\n warn('URDFLoader: resource load error', failedUrl)\n }\n\n loader.load(\n url,\n robot => { robotResult = robot },\n undefined,\n err => reject(err)\n )\n }).catch(err => {\n RealObjectURDF._urdfCache.delete(cacheKey)\n throw err\n })\n RealObjectURDF._urdfCache.set(cacheKey, cached)\n }\n return cached\n }\n\n /** 모든 캐시 비움 (보드 전환 시 호출) */\n static flushCache() {\n RealObjectURDF._urdfCache.clear()\n RealObjectURDF._topViewCache.clear()\n }\n\n static getTopViewCache(source: string): HTMLCanvasElement | undefined {\n return RealObjectURDF._topViewCache.get(source)\n }\n\n static setTopViewCache(source: string, canvas: HTMLCanvasElement) {\n RealObjectURDF._topViewCache.set(source, canvas)\n }\n\n // --- 인스턴스 상태 ---\n\n private _robot?: URDFRobot\n\n // 조인트 인덱스 + 메타 (로드 후 외부에서 구독 가능)\n private _jointIndex = new Map<string, URDFJoint>()\n private _jointMeta = new Map<string, URDFJointMeta>()\n\n // 원본 조인트 값 — reset을 위해 보존\n private _jointOriginals = new Map<string, number>()\n\n // 자동 애니메이션 (sine 모드) 상태\n private _animRaf?: number\n private _animStartTime = 0\n\n // Joint controller (physics mode) 상태\n private _controller?: JointController\n private _ctrlRaf?: number\n private _ctrlLastTime = 0\n\n get robot(): URDFRobot | undefined {\n return this._robot\n }\n\n /** 로드된 조인트 메타 리스트 (property panel 등이 참조) */\n get jointMeta(): URDFJointMeta[] {\n return Array.from(this._jointMeta.values())\n }\n\n get jointNames(): string[] {\n return Array.from(this._jointIndex.keys())\n }\n\n protected _loadExternal(url: string): Promise<URDFRobot> {\n const packages = this.component.state.packages as Record<string, string> | undefined\n return RealObjectURDF.loadURDF(url, packages)\n }\n\n protected _onLoaded(robot: URDFRobot) {\n if (this.component.state.loadError) {\n this.component.setState({ loadError: undefined })\n }\n\n // 캐시의 robot은 공유 참조이므로 clone하여 독립 인스턴스 구성.\n // urdf-loader의 URDFRobot는 Object3D를 상속하므로 Object3D.clone()이 안전하게 동작.\n const cloned = robot.clone() as URDFRobot\n\n // 섀도우 캐스팅\n cloned.traverse(child => {\n if ((child as THREE.Mesh).isMesh) {\n child.castShadow = true\n }\n })\n\n // clear()는 object3d.userData를 리셋하므로 pending 후 재설정.\n this.clear()\n this.object3d.userData.context = this\n\n this._robot = cloned\n\n // 좌표계 변환: URDF(Z-up) → three(Y-up).\n // 사용자가 state.upAxis = 'y' 로 지정하면 변환 생략.\n this.pivot = new THREE.Object3D()\n const upAxis = this.component.state.upAxis\n if (upAxis !== 'y') {\n this.pivot.rotation.x = X_NEG_PI_HALF\n }\n\n // URDF는 기본 meter 단위. state.unitScale로 에디터 단위로 환산.\n const unitScale = this._resolveUnitScale()\n if (unitScale !== 1) {\n this.pivot.scale.setScalar(unitScale)\n }\n\n this.pivot.add(cloned)\n // URDF의 base_link는 관행상 z=0이 바닥(floor origin). things-scene는 center\n // origin이므로, components3D 그룹(= -geometricOffsetY만큼 shift된 바닥 기준\n // subgroup)에 pivot을 넣어 로봇이 컴포넌트 바닥면에 서도록 한다.\n this.components3D.add(this.pivot)\n\n // 조인트 인덱스/메타 구축\n this._buildJointIndex(cloned)\n\n // bounding box 계산 후 updateDimension으로 크기 반영\n const box = new THREE.Box3().setFromObject(this.pivot)\n this._objectSize = box.getSize(new THREE.Vector3())\n\n this.updateDimension()\n\n // 자동 placement 정렬: URDF의 base_link 원점이 로봇 기하의 최저점이 아닐 때\n // (Nao/minitaur/quadrotor는 torso/중심 기준) 로봇 일부가 컴포넌트 범위를\n // 벗어나는 것을 방지. 씬의 placement 모드에 따라 정렬 기준이 다르다.\n // - floor: 로봇 최저점 → 컴포넌트 바닥면\n // - inverted: 로봇 최상점 → 컴포넌트 천장면 (매달림)\n // - space: 정렬 없음 (중심 원점 유지)\n // state.floorAlign === false 로 비활성화 가능.\n if (this.component.state.floorAlign !== false) {\n const placement = this.scenePlacement\n if (placement !== 'space') {\n this.components3D.updateMatrixWorld(true)\n const worldBox = new THREE.Box3().setFromObject(this.pivot)\n if (isFinite(worldBox.min.y) && isFinite(worldBox.max.y)) {\n if (placement === 'inverted') {\n const localMax = this.components3D.worldToLocal(worldBox.max.clone())\n if (localMax.y > 0) {\n this.pivot.position.y -= localMax.y\n }\n } else {\n // floor (기본)\n const localMin = this.components3D.worldToLocal(worldBox.min.clone())\n if (localMin.y < 0) {\n this.pivot.position.y -= localMin.y\n }\n }\n }\n }\n }\n\n // 컨트롤러 초기화 (state.physics 설정된 경우)\n const physicsMode = this.component.state.physics as string | undefined\n if (physicsMode && physicsMode !== 'none') {\n this._setupController(physicsMode)\n }\n\n // 초기 joint 상태 적용\n const jointStates = this.component.state.joints as Record<string, URDFJointState | number> | undefined\n if (jointStates) {\n this._dispatchJointStates(jointStates)\n }\n\n // 자동 애니메이션 시작 (state.autoAnimate === 'sine'인 경우)\n if (this.component.state.autoAnimate === 'sine') {\n this._startAutoAnimate()\n }\n\n // 탑뷰 스냅샷\n const source = this.component.state.src as string\n if (source && !RealObjectURDF._topViewCache.has(source)) {\n try {\n const canvas = RealObjectGLTF.renderTopView(this.pivot)\n RealObjectURDF._topViewCache.set(source, canvas)\n ;(this.component as any)._topViewSnapshot = canvas\n } catch {\n // 스냅샷 실패는 치명적이지 않음\n }\n } else if (source) {\n ;(this.component as any)._topViewSnapshot = RealObjectURDF._topViewCache.get(source)\n }\n }\n\n private _resolveUnitScale(): number {\n const s = this.component.state.unitScale\n if (typeof s === 'number' && s > 0) return s\n // 기본: URDF meter → 에디터 mm 스케일 1000\n return 1000\n }\n\n /**\n * 조인트 트리를 순회하며 인덱스와 메타를 구축한다.\n * fixed 조인트는 조작 대상이 아니므로 메타에서 제외하되 인덱스에는 남긴다.\n */\n private _buildJointIndex(robot: URDFRobot) {\n this._jointIndex.clear()\n this._jointMeta.clear()\n this._jointOriginals.clear()\n\n const joints = robot.joints || {}\n for (const [name, joint] of Object.entries(joints)) {\n this._jointIndex.set(name, joint)\n this._jointOriginals.set(name, joint.angle || 0)\n\n if (joint.jointType === 'fixed') continue\n\n this._jointMeta.set(name, {\n name,\n type: joint.jointType,\n axis: { x: joint.axis.x, y: joint.axis.y, z: joint.axis.z },\n limit: { ...joint.limit }\n })\n }\n }\n\n /**\n * state.joints 변경을 적절한 경로로 디스패치한다.\n * - controller가 활성이면 setTargets (controller가 스무딩/물리 적용)\n * - 없으면 즉시 적용 (direct 모드, 기본)\n */\n private _dispatchJointStates(states: Record<string, URDFJointState | number>) {\n if (this._controller) {\n const targets: Record<string, number> = {}\n for (const [name, raw] of Object.entries(states)) {\n const v = typeof raw === 'number' ? raw : Number((raw as { value?: number } | null)?.value)\n if (Number.isFinite(v)) targets[name] = v\n }\n this._controller.setTargets(targets)\n return\n }\n this._applyJointStates(states)\n }\n\n /**\n * Controller를 인스턴스화하고 setup + tick 루프 시작.\n * 이미 활성 중이면 먼저 해제.\n */\n private _setupController(mode: string) {\n this._disposeController()\n const ctrl = createJointController(mode)\n if (!ctrl) {\n warn(`URDFObject: unknown physics mode '${mode}'`)\n return\n }\n if (!this._robot) return\n\n ctrl.setup({ robot: this._robot, joints: this._jointIndex, jointMeta: this._jointMeta })\n this._controller = ctrl\n\n // 현재 state.joints를 초기 타겟으로 설정\n const s = this.component.state.joints as Record<string, URDFJointState | number> | undefined\n if (s) {\n const targets: Record<string, number> = {}\n for (const [name, raw] of Object.entries(s)) {\n const v = typeof raw === 'number' ? raw : Number((raw as { value?: number } | null)?.value)\n if (Number.isFinite(v)) targets[name] = v\n }\n ctrl.setTargets(targets)\n }\n\n this._ctrlLastTime = performance.now()\n const tick = () => {\n if (!this._controller) {\n this._ctrlRaf = undefined\n return\n }\n const now = performance.now()\n const dt = (now - this._ctrlLastTime) / 1000\n this._ctrlLastTime = now\n const changed = this._controller.tick(dt)\n if (changed) {\n this.component?.invalidate?.()\n }\n this._ctrlRaf = requestAnimationFrame(tick)\n }\n this._ctrlRaf = requestAnimationFrame(tick)\n }\n\n private _disposeController() {\n if (this._ctrlRaf != null) {\n cancelAnimationFrame(this._ctrlRaf)\n this._ctrlRaf = undefined\n }\n if (this._controller) {\n this._controller.dispose()\n this._controller = undefined\n }\n }\n\n /**\n * state.joints 맵을 순회하며 각 조인트 값을 적용한다 (direct 모드).\n * 값은 숫자 또는 { value: number } 형식 모두 허용 (유연성).\n *\n * joint transform 변경 후 component.invalidate()로 재렌더를 요청한다.\n * 표준 경로: invalidate → Layer.throttle_render → rAF __draw__ → trigger('redraw')\n * → ThreeCapability.animate → renderThreeD.\n */\n private _applyJointStates(states: Record<string, URDFJointState | number>) {\n if (!this._robot) return\n\n let changed = false\n for (const [name, raw] of Object.entries(states)) {\n const joint = this._jointIndex.get(name)\n if (!joint || joint.jointType === 'fixed') continue\n\n const value = typeof raw === 'number' ? raw : raw?.value\n if (typeof value !== 'number' || !isFinite(value)) continue\n\n joint.setJointValue(value)\n changed = true\n }\n\n if (changed) {\n this.component?.invalidate?.()\n }\n }\n\n /**\n * 자동 sine 애니메이션 시작.\n * 각 non-fixed 조인트를 limit 범위 내에서 sine 스윙. 조인트 순서에 따라\n * frequency와 phase를 약간씩 다르게 해 동기화를 피하고 유기적 움직임을 만든다.\n *\n * state.joints를 건드리지 않고 joint.setJointValue만 직접 호출 — state 변경\n * cascade를 피해 성능을 유지하고, 슬라이더가 있는 경우에도 state와 시각이\n * 분리되어 표시됨 (활성 시 state는 의미 없음).\n */\n private _startAutoAnimate() {\n if (this._animRaf != null) return\n if (!this._robot || this._jointMeta.size === 0) return\n\n this._animStartTime = performance.now()\n const TWO_PI = Math.PI * 2\n\n const tick = () => {\n if (!this._robot) {\n this._animRaf = undefined\n return\n }\n const t = (performance.now() - this._animStartTime) / 1000\n\n let i = 0\n let changed = false\n for (const [name, meta] of this._jointMeta) {\n const joint = this._jointIndex.get(name)\n if (!joint) continue\n\n const mid = (meta.limit.upper + meta.limit.lower) / 2\n const amp = (meta.limit.upper - meta.limit.lower) / 2\n // 조인트별로 frequency와 phase 약간씩 다르게 (0.15 ~ 0.4 Hz 범위)\n const freq = 0.15 + (i % 6) * 0.05\n const phase = i * 0.73\n const value = mid + amp * Math.sin(t * freq * TWO_PI + phase)\n joint.setJointValue(value)\n changed = true\n i++\n }\n\n if (changed) {\n this.component?.invalidate?.()\n }\n this._animRaf = requestAnimationFrame(tick)\n }\n this._animRaf = requestAnimationFrame(tick)\n }\n\n private _stopAutoAnimate() {\n if (this._animRaf != null) {\n cancelAnimationFrame(this._animRaf)\n this._animRaf = undefined\n }\n }\n\n /**\n * 모든 조인트를 원본 값으로 복원한다.\n */\n private _resetAllJoints() {\n if (!this._robot) return\n for (const [name, orig] of this._jointOriginals) {\n const joint = this._jointIndex.get(name)\n if (joint && joint.jointType !== 'fixed') {\n joint.setJointValue(orig)\n }\n }\n }\n\n // --- 라이프사이클 ---\n\n clear() {\n this._stopAutoAnimate()\n this._disposeController()\n this._jointIndex.clear()\n this._jointMeta.clear()\n this._jointOriginals.clear()\n this._robot = undefined\n\n return super.clear()\n }\n\n updateDimension() {\n if (!this.pivot) return\n\n const { width = 1, height = 1, depth = 1 } = this.component.state\n const { x = 1, y = 1, z = 1 } = this._objectSize || {}\n\n // URDF는 관절 기반 로봇의 비율을 유지해야 하므로 uniform scale을 적용한다.\n // 세 축 각각의 비율 중 가장 작은 값(best-fit)을 사용해 로봇이 컴포넌트의\n // bounding box 안에 왜곡 없이 맞춰지도록 한다.\n const unit = this._resolveUnitScale()\n const baseX = x || 1\n const baseY = y || 1\n const baseZ = z || 1\n const upAxis = this.component.state.upAxis\n const rotated = upAxis !== 'y'\n\n const rX = width / baseX\n const rY = (rotated ? depth : height) / baseY\n const rZ = (rotated ? height : depth) / baseZ\n const ratio = Math.min(rX, rY, rZ)\n\n this.pivot.scale.setScalar(ratio * unit)\n\n this.component.invalidate()\n }\n\n onchange(after: Properties, before: Properties) {\n super.onchange(after, before)\n\n if ('src' in after) {\n this.updateSource()\n return\n }\n\n if ('joints' in after) {\n // 사용자가 joint를 직접 조작하면 자동 애니메이션이 이를 즉시 덮어써\n // 의도를 상쇄하므로, 자동 애니메이션을 우선 중지한다.\n if (this._animRaf != null) {\n this._stopAutoAnimate()\n this._component.setState({ autoAnimate: 'none' })\n }\n const jointStates = after.joints as Record<string, URDFJointState | number> | undefined\n if (this._controller) {\n // controller 모드: 타겟만 갱신. 이전 값 리셋은 controller가 연속 감쇠로\n // 알아서 수렴하므로 불필요.\n if (jointStates && Object.keys(jointStates).length > 0) {\n this._dispatchJointStates(jointStates)\n }\n } else {\n // direct 모드: 전체 reset 후 명시된 값만 재적용 (sparse state 지원).\n this._resetAllJoints()\n if (jointStates && Object.keys(jointStates).length > 0) {\n this._applyJointStates(jointStates)\n }\n }\n }\n\n if ('autoAnimate' in after) {\n if (after.autoAnimate === 'sine') {\n this._startAutoAnimate()\n } else {\n this._stopAutoAnimate()\n }\n }\n\n if ('physics' in after) {\n const mode = after.physics as string | undefined\n if (mode && mode !== 'none') {\n this._setupController(mode)\n } else {\n this._disposeController()\n }\n }\n\n if ('upAxis' in after || 'unitScale' in after || 'packages' in after) {\n // 좌표/스케일/패키지 매핑 변경은 pivot 재구성이 필요하므로 전체 재빌드\n this.build()\n }\n }\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import type { JointController, JointControllerContext } from './joint-controller.js';
2
+ export declare class SmoothingController implements JointController {
3
+ private _joints?;
4
+ private _current;
5
+ private _target;
6
+ private _velocity;
7
+ /** 응답 주파수 (rad/s). 클수록 빠른 수렴. */
8
+ omega: number;
9
+ /** 수렴 판정 임계값. error와 velocity가 모두 이 값 미만이면 그 조인트는 skip. */
10
+ epsilon: number;
11
+ setup(ctx: JointControllerContext): void;
12
+ setTargets(targets: Record<string, number>): void;
13
+ tick(dt: number): boolean;
14
+ dispose(): void;
15
+ }
@@ -0,0 +1,88 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * Level 1 관절 스무딩 — 스크립트된 애니메이션이 아닌 "타겟 추적 감쇠".
5
+ *
6
+ * 구현: critically damped 2차 spring-damper ODE.
7
+ * acc = error × ω² - 2 × ω × vel
8
+ * vel += acc × dt
9
+ * pos += vel × dt
10
+ * 여기서 ω는 응답 주파수(rad/s). 기본 ω=10이면 시상수 ~0.1s 수준으로 빠르게
11
+ * 수렴하면서도 overshoot 없음 (ζ=1).
12
+ *
13
+ * 사용자가 state.joints를 바꾸면 setTargets로 새 목표가 전달되고, tick에서
14
+ * 매 프레임 current가 target으로 수렴한다. 바뀐 값은 joint.setJointValue로
15
+ * 직접 적용.
16
+ */
17
+ import { registerJointController } from './joint-controller.js';
18
+ export class SmoothingController {
19
+ _joints;
20
+ _current = new Map();
21
+ _target = new Map();
22
+ _velocity = new Map();
23
+ /** 응답 주파수 (rad/s). 클수록 빠른 수렴. */
24
+ omega = 10;
25
+ /** 수렴 판정 임계값. error와 velocity가 모두 이 값 미만이면 그 조인트는 skip. */
26
+ epsilon = 1e-5;
27
+ setup(ctx) {
28
+ this._joints = ctx.joints;
29
+ this._current.clear();
30
+ this._target.clear();
31
+ this._velocity.clear();
32
+ for (const [name, joint] of ctx.joints) {
33
+ const v = joint.angle ?? 0;
34
+ this._current.set(name, v);
35
+ this._target.set(name, v);
36
+ this._velocity.set(name, 0);
37
+ }
38
+ }
39
+ setTargets(targets) {
40
+ for (const [name, raw] of Object.entries(targets)) {
41
+ const v = typeof raw === 'number' ? raw : Number(raw?.value);
42
+ if (Number.isFinite(v)) {
43
+ this._target.set(name, v);
44
+ }
45
+ }
46
+ }
47
+ tick(dt) {
48
+ if (!this._joints)
49
+ return false;
50
+ // dt 보정: 0 이하거나 비정상적으로 크면 (탭 전환 후 복귀 등) 스킵/클램프
51
+ if (dt <= 0)
52
+ return false;
53
+ if (dt > 0.1)
54
+ dt = 0.016;
55
+ const w = this.omega;
56
+ const w2 = w * w;
57
+ const twoW = 2 * w;
58
+ const eps = this.epsilon;
59
+ let changed = false;
60
+ for (const [name, joint] of this._joints) {
61
+ if (joint.jointType === 'fixed')
62
+ continue;
63
+ const target = this._target.get(name) ?? 0;
64
+ const cur = this._current.get(name) ?? target;
65
+ const vel = this._velocity.get(name) ?? 0;
66
+ const error = target - cur;
67
+ if (Math.abs(error) < eps && Math.abs(vel) < eps)
68
+ continue;
69
+ const acc = error * w2 - twoW * vel;
70
+ const newVel = vel + acc * dt;
71
+ const newCur = cur + newVel * dt;
72
+ this._current.set(name, newCur);
73
+ this._velocity.set(name, newVel);
74
+ joint.setJointValue(newCur);
75
+ changed = true;
76
+ }
77
+ return changed;
78
+ }
79
+ dispose() {
80
+ this._joints = undefined;
81
+ this._current.clear();
82
+ this._target.clear();
83
+ this._velocity.clear();
84
+ }
85
+ }
86
+ // 기본 등록. 'smooth' 이름으로 state.physics에서 선택 가능.
87
+ registerJointController('smooth', () => new SmoothingController());
88
+ //# sourceMappingURL=smoothing-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smoothing-controller.js","sourceRoot":"","sources":["../src/smoothing-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * Level 1 관절 스무딩 — 스크립트된 애니메이션이 아닌 \"타겟 추적 감쇠\".\n *\n * 구현: critically damped 2차 spring-damper ODE.\n * acc = error × ω² - 2 × ω × vel\n * vel += acc × dt\n * pos += vel × dt\n * 여기서 ω는 응답 주파수(rad/s). 기본 ω=10이면 시상수 ~0.1s 수준으로 빠르게\n * 수렴하면서도 overshoot 없음 (ζ=1).\n *\n * 사용자가 state.joints를 바꾸면 setTargets로 새 목표가 전달되고, tick에서\n * 매 프레임 current가 target으로 수렴한다. 바뀐 값은 joint.setJointValue로\n * 직접 적용.\n */\n\nimport type { URDFJoint } from 'urdf-loader'\nimport type { JointController, JointControllerContext } from './joint-controller.js'\nimport { registerJointController } from './joint-controller.js'\n\nexport class SmoothingController implements JointController {\n private _joints?: Map<string, URDFJoint>\n private _current = new Map<string, number>()\n private _target = new Map<string, number>()\n private _velocity = new Map<string, number>()\n\n /** 응답 주파수 (rad/s). 클수록 빠른 수렴. */\n omega: number = 10\n\n /** 수렴 판정 임계값. error와 velocity가 모두 이 값 미만이면 그 조인트는 skip. */\n epsilon: number = 1e-5\n\n setup(ctx: JointControllerContext): void {\n this._joints = ctx.joints\n this._current.clear()\n this._target.clear()\n this._velocity.clear()\n\n for (const [name, joint] of ctx.joints) {\n const v = joint.angle ?? 0\n this._current.set(name, v)\n this._target.set(name, v)\n this._velocity.set(name, 0)\n }\n }\n\n setTargets(targets: Record<string, number>): void {\n for (const [name, raw] of Object.entries(targets)) {\n const v = typeof raw === 'number' ? raw : Number((raw as { value?: number } | null)?.value)\n if (Number.isFinite(v)) {\n this._target.set(name, v)\n }\n }\n }\n\n tick(dt: number): boolean {\n if (!this._joints) return false\n // dt 보정: 0 이하거나 비정상적으로 크면 (탭 전환 후 복귀 등) 스킵/클램프\n if (dt <= 0) return false\n if (dt > 0.1) dt = 0.016\n\n const w = this.omega\n const w2 = w * w\n const twoW = 2 * w\n const eps = this.epsilon\n let changed = false\n\n for (const [name, joint] of this._joints) {\n if (joint.jointType === 'fixed') continue\n\n const target = this._target.get(name) ?? 0\n const cur = this._current.get(name) ?? target\n const vel = this._velocity.get(name) ?? 0\n const error = target - cur\n\n if (Math.abs(error) < eps && Math.abs(vel) < eps) continue\n\n const acc = error * w2 - twoW * vel\n const newVel = vel + acc * dt\n const newCur = cur + newVel * dt\n\n this._current.set(name, newCur)\n this._velocity.set(name, newVel)\n joint.setJointValue(newCur)\n changed = true\n }\n\n return changed\n }\n\n dispose(): void {\n this._joints = undefined\n this._current.clear()\n this._target.clear()\n this._velocity.clear()\n }\n}\n\n// 기본 등록. 'smooth' 이름으로 state.physics에서 선택 가능.\nregisterJointController('smooth', () => new SmoothingController())\n"]}
@@ -11,7 +11,7 @@ export default {
11
11
  top: 10,
12
12
  width: 200,
13
13
  height: 200,
14
- depth: 500,
14
+ depth: 200,
15
15
  upAxis: 'z',
16
16
  unitScale: 1000
17
17
  }
@@ -1 +1 @@
1
- {"version":3,"file":"urdf.js","sourceRoot":"","sources":["../../src/templates/urdf.ts"],"names":[],"mappings":"AAAA,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,sBAAsB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAElE,eAAe;IACb,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,MAAM;IACnB,KAAK,EAAE,IAAI;IACX,gGAAgG;IAChG,IAAI;IACJ,KAAK,EAAE;QACL,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,EAAE;QACR,GAAG,EAAE,EAAE;QACP,KAAK,EAAE,GAAG;QACV,MAAM,EAAE,GAAG;QACX,KAAK,EAAE,GAAG;QACV,MAAM,EAAE,GAAG;QACX,SAAS,EAAE,IAAI;KAChB;CACF,CAAA","sourcesContent":["const icon = new URL('../../icons/urdf.png', import.meta.url).href\n\nexport default {\n type: 'urdf',\n description: 'urdf',\n group: '3D',\n /* line|shape|textAndMedia|chartAndGauge|table|container|dataSource|IoT|3D|warehouse|form|etc */\n icon,\n model: {\n type: 'urdf',\n left: 10,\n top: 10,\n width: 200,\n height: 200,\n depth: 500,\n upAxis: 'z',\n unitScale: 1000\n }\n}\n"]}
1
+ {"version":3,"file":"urdf.js","sourceRoot":"","sources":["../../src/templates/urdf.ts"],"names":[],"mappings":"AAAA,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,sBAAsB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAA;AAElE,eAAe;IACb,IAAI,EAAE,MAAM;IACZ,WAAW,EAAE,MAAM;IACnB,KAAK,EAAE,IAAI;IACX,gGAAgG;IAChG,IAAI;IACJ,KAAK,EAAE;QACL,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,EAAE;QACR,GAAG,EAAE,EAAE;QACP,KAAK,EAAE,GAAG;QACV,MAAM,EAAE,GAAG;QACX,KAAK,EAAE,GAAG;QACV,MAAM,EAAE,GAAG;QACX,SAAS,EAAE,IAAI;KAChB;CACF,CAAA","sourcesContent":["const icon = new URL('../../icons/urdf.png', import.meta.url).href\n\nexport default {\n type: 'urdf',\n description: 'urdf',\n group: '3D',\n /* line|shape|textAndMedia|chartAndGauge|table|container|dataSource|IoT|3D|warehouse|form|etc */\n icon,\n model: {\n type: 'urdf',\n left: 10,\n top: 10,\n width: 200,\n height: 200,\n depth: 200,\n upAxis: 'z',\n unitScale: 1000\n }\n}\n"]}