@operato/scene-urdf 10.0.0-beta.1 → 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.
- package/TODO.md +58 -0
- package/dist/editors/index.d.ts +7 -0
- package/dist/editors/index.js +12 -1
- package/dist/editors/index.js.map +1 -1
- package/dist/editors/property-editor-urdf-joints.d.ts +54 -0
- package/dist/editors/property-editor-urdf-joints.js +246 -0
- package/dist/editors/property-editor-urdf-joints.js.map +1 -0
- package/dist/editors/property-editor-urdf-preset.d.ts +8 -0
- package/dist/editors/property-editor-urdf-preset.js +114 -0
- package/dist/editors/property-editor-urdf-preset.js.map +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +22 -2
- package/dist/index.js.map +1 -1
- package/dist/joint-controller.d.ts +35 -0
- package/dist/joint-controller.js +34 -0
- package/dist/joint-controller.js.map +1 -0
- package/dist/real-object-urdf.d.ts +106 -0
- package/dist/real-object-urdf.js +527 -0
- package/dist/real-object-urdf.js.map +1 -0
- package/dist/smoothing-controller.d.ts +15 -0
- package/dist/smoothing-controller.js +88 -0
- package/dist/smoothing-controller.js.map +1 -0
- package/dist/templates/index.d.ts +3 -0
- package/dist/templates/index.js +2 -3
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/{urdf-controller.d.ts → urdf.d.ts} +3 -0
- package/dist/templates/urdf.js +19 -0
- package/dist/templates/urdf.js.map +1 -0
- package/dist/urdf-object.d.ts +264 -0
- package/dist/urdf-object.js +190 -0
- package/dist/urdf-object.js.map +1 -0
- package/dist/urdf-presets.d.ts +22 -0
- package/dist/urdf-presets.js +176 -0
- package/dist/urdf-presets.js.map +1 -0
- package/icons/urdf.png +0 -0
- package/package.json +5 -4
- package/translations/en.json +10 -16
- package/translations/ja.json +10 -16
- package/translations/ko.json +10 -16
- package/translations/ms.json +10 -16
- package/translations/zh.json +10 -16
- package/dist/elements/drag-n-drop.d.ts +0 -2
- package/dist/elements/drag-n-drop.js +0 -126
- package/dist/elements/drag-n-drop.js.map +0 -1
- package/dist/elements/urdf-controller-element.d.ts +0 -12
- package/dist/elements/urdf-controller-element.js +0 -283
- package/dist/elements/urdf-controller-element.js.map +0 -1
- package/dist/elements/urdf-drag-controls.d.ts +0 -32
- package/dist/elements/urdf-drag-controls.js +0 -197
- package/dist/elements/urdf-drag-controls.js.map +0 -1
- package/dist/elements/urdf-manipulator-element.d.ts +0 -15
- package/dist/elements/urdf-manipulator-element.js +0 -112
- package/dist/elements/urdf-manipulator-element.js.map +0 -1
- package/dist/elements/urdf-viewer-element.d.ts +0 -53
- package/dist/elements/urdf-viewer-element.js +0 -414
- package/dist/elements/urdf-viewer-element.js.map +0 -1
- package/dist/templates/urdf-controller.js +0 -16
- package/dist/templates/urdf-controller.js.map +0 -1
- package/dist/templates/urdf-viewer.d.ts +0 -14
- package/dist/templates/urdf-viewer.js +0 -16
- package/dist/templates/urdf-viewer.js.map +0 -1
- package/dist/urdf-controller.d.ts +0 -15
- package/dist/urdf-controller.js +0 -70
- package/dist/urdf-controller.js.map +0 -1
- package/dist/urdf-viewer.d.ts +0 -16
- package/dist/urdf-viewer.js +0 -202
- package/dist/urdf-viewer.js.map +0 -1
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* URDF 조인트 제어기 추상화.
|
|
5
|
+
*
|
|
6
|
+
* 배경:
|
|
7
|
+
* state.joints는 조인트의 "목표값"을 표현하고, 실제 URDFJoint에 적용되는 값은
|
|
8
|
+
* 어떤 정책을 따를지에 따라 달라질 수 있다.
|
|
9
|
+
* - Direct: 목표값을 즉시 적용 (현재 기본 동작)
|
|
10
|
+
* - Smooth: 스프링-댐퍼로 부드럽게 수렴
|
|
11
|
+
* - Rapier 등 물리: rigid body 시뮬레이션의 결과를 관절에 반영
|
|
12
|
+
*
|
|
13
|
+
* 이들은 모두 "목표값 → 실제 적용값" 매핑이라는 공통 추상으로 묶을 수 있다.
|
|
14
|
+
*
|
|
15
|
+
* 확장:
|
|
16
|
+
* 외부 패키지가 `registerJointController('rapier', ...)` 형태로 구현을 등록해
|
|
17
|
+
* urdf 컴포넌트의 state.physics 값을 통해 선택 가능. 예를 들어 미래의
|
|
18
|
+
* `@operato/scene-urdf-rapier` 패키지가 Rapier 기반 controller를 register.
|
|
19
|
+
*/
|
|
20
|
+
const registry = new Map();
|
|
21
|
+
/**
|
|
22
|
+
* 외부 패키지가 자신의 controller 구현을 등록한다.
|
|
23
|
+
* 예: `registerJointController('rapier', () => new RapierJointController())`.
|
|
24
|
+
*/
|
|
25
|
+
export function registerJointController(name, factory) {
|
|
26
|
+
registry.set(name, factory);
|
|
27
|
+
}
|
|
28
|
+
export function createJointController(name) {
|
|
29
|
+
return registry.get(name)?.();
|
|
30
|
+
}
|
|
31
|
+
export function listJointControllers() {
|
|
32
|
+
return [...registry.keys()];
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=joint-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"joint-controller.js","sourceRoot":"","sources":["../src/joint-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAqCH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkC,CAAA;AAE1D;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAAY,EAAE,OAA+B;IACnF,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;AAC7B,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,CAAA;AAC/B,CAAC;AAED,MAAM,UAAU,oBAAoB;IAClC,OAAO,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;AAC7B,CAAC","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * URDF 조인트 제어기 추상화.\n *\n * 배경:\n * state.joints는 조인트의 \"목표값\"을 표현하고, 실제 URDFJoint에 적용되는 값은\n * 어떤 정책을 따를지에 따라 달라질 수 있다.\n * - Direct: 목표값을 즉시 적용 (현재 기본 동작)\n * - Smooth: 스프링-댐퍼로 부드럽게 수렴\n * - Rapier 등 물리: rigid body 시뮬레이션의 결과를 관절에 반영\n *\n * 이들은 모두 \"목표값 → 실제 적용값\" 매핑이라는 공통 추상으로 묶을 수 있다.\n *\n * 확장:\n * 외부 패키지가 `registerJointController('rapier', ...)` 형태로 구현을 등록해\n * urdf 컴포넌트의 state.physics 값을 통해 선택 가능. 예를 들어 미래의\n * `@operato/scene-urdf-rapier` 패키지가 Rapier 기반 controller를 register.\n */\n\nimport type { URDFRobot, URDFJoint } from 'urdf-loader'\nimport type { URDFJointMeta } from './real-object-urdf.js'\n\nexport interface JointControllerContext {\n robot: URDFRobot\n /** non-fixed + fixed 포함 모든 조인트 */\n joints: Map<string, URDFJoint>\n /** non-fixed 조인트의 메타 (type, axis, limit) — fixed는 제외됨 */\n jointMeta: Map<string, URDFJointMeta>\n}\n\nexport interface JointController {\n /** 로봇 로드 직후 1회 호출. 내부 상태(current/target/velocity 등) 초기화. */\n setup(ctx: JointControllerContext): void\n\n /**\n * state.joints 변경 시 호출. controller가 `targets` 맵을 내부 저장하고\n * tick에서 실제 적용을 결정한다. 각 value는 joint의 internal unit\n * (revolute=rad, prismatic=m).\n */\n setTargets(targets: Record<string, number>): void\n\n /**\n * 매 프레임 호출 (requestAnimationFrame). dt는 초 단위.\n * 관절에 값을 적용(joint.setJointValue)하고, 값이 실제로 변경됐다면 true를\n * 반환 (상위가 invalidate를 결정).\n */\n tick(dt: number): boolean\n\n /** 로봇 해제 시 호출. 내부 자원/캐시 정리. */\n dispose(): void\n}\n\nexport type JointControllerFactory = () => JointController\n\nconst registry = new Map<string, JointControllerFactory>()\n\n/**\n * 외부 패키지가 자신의 controller 구현을 등록한다.\n * 예: `registerJointController('rapier', () => new RapierJointController())`.\n */\nexport function registerJointController(name: string, factory: JointControllerFactory): void {\n registry.set(name, factory)\n}\n\nexport function createJointController(name: string): JointController | undefined {\n return registry.get(name)?.()\n}\n\nexport function listJointControllers(): string[] {\n return [...registry.keys()]\n}\n"]}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as THREE from 'three';
|
|
2
|
+
import type { URDFRobot } from 'urdf-loader';
|
|
3
|
+
import type { Properties } from '@hatiolab/things-scene';
|
|
4
|
+
import { RealObjectExternalModel } from '@hatiolab/things-scene';
|
|
5
|
+
export interface URDFJointState {
|
|
6
|
+
/** revolute/continuous: 라디안. prismatic: 거리(URDF 단위). */
|
|
7
|
+
value?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface URDFJointMeta {
|
|
10
|
+
name: string;
|
|
11
|
+
type: 'fixed' | 'continuous' | 'revolute' | 'planar' | 'prismatic' | 'floating';
|
|
12
|
+
axis: {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
z: number;
|
|
16
|
+
};
|
|
17
|
+
limit: {
|
|
18
|
+
lower: number;
|
|
19
|
+
upper: number;
|
|
20
|
+
effort: number;
|
|
21
|
+
velocity: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare class RealObjectURDF extends RealObjectExternalModel<URDFRobot> {
|
|
25
|
+
protected get _formatLabel(): string;
|
|
26
|
+
protected get _placeholderColor(): number;
|
|
27
|
+
private static _urdfCache;
|
|
28
|
+
private static _topViewCache;
|
|
29
|
+
/**
|
|
30
|
+
* URDF를 로드한다 (동일 URL 캐시).
|
|
31
|
+
* mesh 로더는 STL/DAE(내장) + OBJ(MTL 연동) + GLB(확장)를 지원한다.
|
|
32
|
+
*
|
|
33
|
+
* URDFLoader.loadAsync는 URDF XML 파싱 직후 resolve하지만 mesh(STL/DAE/GLB)는
|
|
34
|
+
* 비동기로 뒤늦게 attach된다. robot.clone() 시점에 mesh가 아직 없으면 클론된
|
|
35
|
+
* 트리엔 visual이 비게 되고 원본에만 mesh가 붙는다. 전용 LoadingManager의
|
|
36
|
+
* onLoad를 사용해 URDF + 모든 mesh가 완료된 후 resolve한다.
|
|
37
|
+
*
|
|
38
|
+
* `packages` 옵션을 통해 ROS `package://<pkg>/<rel>` prefix 해석을 설정할 수
|
|
39
|
+
* 있다 (예: `{ a1_description: 'https://.../robots/a1_description' }`).
|
|
40
|
+
*/
|
|
41
|
+
static loadURDF(url: string, packages?: Record<string, string>): Promise<URDFRobot>;
|
|
42
|
+
/** 모든 캐시 비움 (보드 전환 시 호출) */
|
|
43
|
+
static flushCache(): void;
|
|
44
|
+
static getTopViewCache(source: string): HTMLCanvasElement | undefined;
|
|
45
|
+
static setTopViewCache(source: string, canvas: HTMLCanvasElement): void;
|
|
46
|
+
private _robot?;
|
|
47
|
+
private _jointIndex;
|
|
48
|
+
private _jointMeta;
|
|
49
|
+
private _jointOriginals;
|
|
50
|
+
private _animRaf?;
|
|
51
|
+
private _animStartTime;
|
|
52
|
+
private _controller?;
|
|
53
|
+
private _ctrlRaf?;
|
|
54
|
+
private _ctrlLastTime;
|
|
55
|
+
get robot(): URDFRobot | undefined;
|
|
56
|
+
/** 로드된 조인트 메타 리스트 (property panel 등이 참조) */
|
|
57
|
+
get jointMeta(): URDFJointMeta[];
|
|
58
|
+
get jointNames(): string[];
|
|
59
|
+
protected _loadExternal(url: string): Promise<URDFRobot>;
|
|
60
|
+
protected _onLoaded(robot: URDFRobot): void;
|
|
61
|
+
private _resolveUnitScale;
|
|
62
|
+
/**
|
|
63
|
+
* 조인트 트리를 순회하며 인덱스와 메타를 구축한다.
|
|
64
|
+
* fixed 조인트는 조작 대상이 아니므로 메타에서 제외하되 인덱스에는 남긴다.
|
|
65
|
+
*/
|
|
66
|
+
private _buildJointIndex;
|
|
67
|
+
/**
|
|
68
|
+
* state.joints 변경을 적절한 경로로 디스패치한다.
|
|
69
|
+
* - controller가 활성이면 setTargets (controller가 스무딩/물리 적용)
|
|
70
|
+
* - 없으면 즉시 적용 (direct 모드, 기본)
|
|
71
|
+
*/
|
|
72
|
+
private _dispatchJointStates;
|
|
73
|
+
/**
|
|
74
|
+
* Controller를 인스턴스화하고 setup + tick 루프 시작.
|
|
75
|
+
* 이미 활성 중이면 먼저 해제.
|
|
76
|
+
*/
|
|
77
|
+
private _setupController;
|
|
78
|
+
private _disposeController;
|
|
79
|
+
/**
|
|
80
|
+
* state.joints 맵을 순회하며 각 조인트 값을 적용한다 (direct 모드).
|
|
81
|
+
* 값은 숫자 또는 { value: number } 형식 모두 허용 (유연성).
|
|
82
|
+
*
|
|
83
|
+
* joint transform 변경 후 component.invalidate()로 재렌더를 요청한다.
|
|
84
|
+
* 표준 경로: invalidate → Layer.throttle_render → rAF __draw__ → trigger('redraw')
|
|
85
|
+
* → ThreeCapability.animate → renderThreeD.
|
|
86
|
+
*/
|
|
87
|
+
private _applyJointStates;
|
|
88
|
+
/**
|
|
89
|
+
* 자동 sine 애니메이션 시작.
|
|
90
|
+
* 각 non-fixed 조인트를 limit 범위 내에서 sine 스윙. 조인트 순서에 따라
|
|
91
|
+
* frequency와 phase를 약간씩 다르게 해 동기화를 피하고 유기적 움직임을 만든다.
|
|
92
|
+
*
|
|
93
|
+
* state.joints를 건드리지 않고 joint.setJointValue만 직접 호출 — state 변경
|
|
94
|
+
* cascade를 피해 성능을 유지하고, 슬라이더가 있는 경우에도 state와 시각이
|
|
95
|
+
* 분리되어 표시됨 (활성 시 state는 의미 없음).
|
|
96
|
+
*/
|
|
97
|
+
private _startAutoAnimate;
|
|
98
|
+
private _stopAutoAnimate;
|
|
99
|
+
/**
|
|
100
|
+
* 모든 조인트를 원본 값으로 복원한다.
|
|
101
|
+
*/
|
|
102
|
+
private _resetAllJoints;
|
|
103
|
+
clear(): THREE.Object3D<THREE.Object3DEventMap>;
|
|
104
|
+
updateDimension(): void;
|
|
105
|
+
onchange(after: Properties, before: Properties): void;
|
|
106
|
+
}
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © HatioLab Inc. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* URDF (Unified Robot Description Format) 지원 RealObject.
|
|
5
|
+
* RealObjectGLTF와 동일한 캐시/레이스/placeholder 패턴을 따른다.
|
|
6
|
+
*
|
|
7
|
+
* 차이점: GLTF는 노드/애니메이션이 주된 바인딩 대상이지만, URDF는 "joint"가
|
|
8
|
+
* 1급 시민이다. state.joints에 이름→라디안(또는 prismatic의 경우 거리) 맵을
|
|
9
|
+
* 두어 외부 센서값 등을 연결할 수 있도록 한다.
|
|
10
|
+
*
|
|
11
|
+
* 좌표계: URDF는 Z-up, things-scene의 3D는 Y-up. 로드 후 루트 그룹에 1회만
|
|
12
|
+
* -PI/2 X축 회전을 적용하여 변환한다. 내부 조인트 축은 원본 그대로 유지.
|
|
13
|
+
*/
|
|
14
|
+
import * as THREE from 'three';
|
|
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';
|
|
18
|
+
import { warn } from '@hatiolab/things-scene';
|
|
19
|
+
import { RealObjectExternalModel } from '@hatiolab/things-scene';
|
|
20
|
+
import { RealObjectGLTF } from '@hatiolab/things-scene';
|
|
21
|
+
import { createJointController } from './joint-controller.js';
|
|
22
|
+
const X_NEG_PI_HALF = -Math.PI / 2;
|
|
23
|
+
export class RealObjectURDF extends RealObjectExternalModel {
|
|
24
|
+
get _formatLabel() { return 'urdf'; }
|
|
25
|
+
get _placeholderColor() { return 0x1976d2; }
|
|
26
|
+
// URDF 파싱 결과 캐시 — 동일 URL 재파싱 방지. clone()으로 복제 사용.
|
|
27
|
+
static _urdfCache = new Map();
|
|
28
|
+
// 탑뷰 스냅샷 캐시 (2D 렌더 폴백)
|
|
29
|
+
static _topViewCache = new Map();
|
|
30
|
+
/**
|
|
31
|
+
* URDF를 로드한다 (동일 URL 캐시).
|
|
32
|
+
* mesh 로더는 STL/DAE(내장) + OBJ(MTL 연동) + GLB(확장)를 지원한다.
|
|
33
|
+
*
|
|
34
|
+
* URDFLoader.loadAsync는 URDF XML 파싱 직후 resolve하지만 mesh(STL/DAE/GLB)는
|
|
35
|
+
* 비동기로 뒤늦게 attach된다. robot.clone() 시점에 mesh가 아직 없으면 클론된
|
|
36
|
+
* 트리엔 visual이 비게 되고 원본에만 mesh가 붙는다. 전용 LoadingManager의
|
|
37
|
+
* onLoad를 사용해 URDF + 모든 mesh가 완료된 후 resolve한다.
|
|
38
|
+
*
|
|
39
|
+
* `packages` 옵션을 통해 ROS `package://<pkg>/<rel>` prefix 해석을 설정할 수
|
|
40
|
+
* 있다 (예: `{ a1_description: 'https://.../robots/a1_description' }`).
|
|
41
|
+
*/
|
|
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);
|
|
48
|
+
if (!cached) {
|
|
49
|
+
cached = new Promise((resolve, reject) => {
|
|
50
|
+
const manager = new THREE.LoadingManager();
|
|
51
|
+
const loader = new URDFLoader(manager);
|
|
52
|
+
if (packages) {
|
|
53
|
+
loader.packages = packages;
|
|
54
|
+
}
|
|
55
|
+
loader.loadMeshCb = ((path, mgr, done) => {
|
|
56
|
+
if (/\.(glb|gltf)$/i.test(path)) {
|
|
57
|
+
// GLB는 별도 시스템 — manager 추적에서 벗어나므로 명시적으로 track.
|
|
58
|
+
mgr.itemStart(path);
|
|
59
|
+
RealObjectGLTF.loadGLTF(path).then(gltf => { done(gltf.scene.clone()); mgr.itemEnd(path); }, err => { done(null, err); mgr.itemError(path); mgr.itemEnd(path); });
|
|
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
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// STL/DAE는 urdf-loader 내장 디폴트로 폴백 (manager 자동 추적)
|
|
90
|
+
;
|
|
91
|
+
loader.defaultMeshLoader(path, mgr, done);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
let robotResult;
|
|
95
|
+
manager.onLoad = () => {
|
|
96
|
+
if (robotResult)
|
|
97
|
+
resolve(robotResult);
|
|
98
|
+
else
|
|
99
|
+
reject(new Error('URDF load completed but no robot result'));
|
|
100
|
+
};
|
|
101
|
+
manager.onError = (failedUrl) => {
|
|
102
|
+
// 개별 에러는 로그로만 남김 — 전체 로드는 그래도 완료되도록
|
|
103
|
+
// (일부 mesh 실패 시 나머지라도 보이게)
|
|
104
|
+
warn('URDFLoader: resource load error', failedUrl);
|
|
105
|
+
};
|
|
106
|
+
loader.load(url, robot => { robotResult = robot; }, undefined, err => reject(err));
|
|
107
|
+
}).catch(err => {
|
|
108
|
+
RealObjectURDF._urdfCache.delete(cacheKey);
|
|
109
|
+
throw err;
|
|
110
|
+
});
|
|
111
|
+
RealObjectURDF._urdfCache.set(cacheKey, cached);
|
|
112
|
+
}
|
|
113
|
+
return cached;
|
|
114
|
+
}
|
|
115
|
+
/** 모든 캐시 비움 (보드 전환 시 호출) */
|
|
116
|
+
static flushCache() {
|
|
117
|
+
RealObjectURDF._urdfCache.clear();
|
|
118
|
+
RealObjectURDF._topViewCache.clear();
|
|
119
|
+
}
|
|
120
|
+
static getTopViewCache(source) {
|
|
121
|
+
return RealObjectURDF._topViewCache.get(source);
|
|
122
|
+
}
|
|
123
|
+
static setTopViewCache(source, canvas) {
|
|
124
|
+
RealObjectURDF._topViewCache.set(source, canvas);
|
|
125
|
+
}
|
|
126
|
+
// --- 인스턴스 상태 ---
|
|
127
|
+
_robot;
|
|
128
|
+
// 조인트 인덱스 + 메타 (로드 후 외부에서 구독 가능)
|
|
129
|
+
_jointIndex = new Map();
|
|
130
|
+
_jointMeta = new Map();
|
|
131
|
+
// 원본 조인트 값 — reset을 위해 보존
|
|
132
|
+
_jointOriginals = new Map();
|
|
133
|
+
// 자동 애니메이션 (sine 모드) 상태
|
|
134
|
+
_animRaf;
|
|
135
|
+
_animStartTime = 0;
|
|
136
|
+
// Joint controller (physics mode) 상태
|
|
137
|
+
_controller;
|
|
138
|
+
_ctrlRaf;
|
|
139
|
+
_ctrlLastTime = 0;
|
|
140
|
+
get robot() {
|
|
141
|
+
return this._robot;
|
|
142
|
+
}
|
|
143
|
+
/** 로드된 조인트 메타 리스트 (property panel 등이 참조) */
|
|
144
|
+
get jointMeta() {
|
|
145
|
+
return Array.from(this._jointMeta.values());
|
|
146
|
+
}
|
|
147
|
+
get jointNames() {
|
|
148
|
+
return Array.from(this._jointIndex.keys());
|
|
149
|
+
}
|
|
150
|
+
_loadExternal(url) {
|
|
151
|
+
const packages = this.component.state.packages;
|
|
152
|
+
return RealObjectURDF.loadURDF(url, packages);
|
|
153
|
+
}
|
|
154
|
+
_onLoaded(robot) {
|
|
155
|
+
if (this.component.state.loadError) {
|
|
156
|
+
this.component.setState({ loadError: undefined });
|
|
157
|
+
}
|
|
158
|
+
// 캐시의 robot은 공유 참조이므로 clone하여 독립 인스턴스 구성.
|
|
159
|
+
// urdf-loader의 URDFRobot는 Object3D를 상속하므로 Object3D.clone()이 안전하게 동작.
|
|
160
|
+
const cloned = robot.clone();
|
|
161
|
+
// 섀도우 캐스팅
|
|
162
|
+
cloned.traverse(child => {
|
|
163
|
+
if (child.isMesh) {
|
|
164
|
+
child.castShadow = true;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
// clear()는 object3d.userData를 리셋하므로 pending 후 재설정.
|
|
168
|
+
this.clear();
|
|
169
|
+
this.object3d.userData.context = this;
|
|
170
|
+
this._robot = cloned;
|
|
171
|
+
// 좌표계 변환: URDF(Z-up) → three(Y-up).
|
|
172
|
+
// 사용자가 state.upAxis = 'y' 로 지정하면 변환 생략.
|
|
173
|
+
this.pivot = new THREE.Object3D();
|
|
174
|
+
const upAxis = this.component.state.upAxis;
|
|
175
|
+
if (upAxis !== 'y') {
|
|
176
|
+
this.pivot.rotation.x = X_NEG_PI_HALF;
|
|
177
|
+
}
|
|
178
|
+
// URDF는 기본 meter 단위. state.unitScale로 에디터 단위로 환산.
|
|
179
|
+
const unitScale = this._resolveUnitScale();
|
|
180
|
+
if (unitScale !== 1) {
|
|
181
|
+
this.pivot.scale.setScalar(unitScale);
|
|
182
|
+
}
|
|
183
|
+
this.pivot.add(cloned);
|
|
184
|
+
// URDF의 base_link는 관행상 z=0이 바닥(floor origin). things-scene는 center
|
|
185
|
+
// origin이므로, components3D 그룹(= -geometricOffsetY만큼 shift된 바닥 기준
|
|
186
|
+
// subgroup)에 pivot을 넣어 로봇이 컴포넌트 바닥면에 서도록 한다.
|
|
187
|
+
this.components3D.add(this.pivot);
|
|
188
|
+
// 조인트 인덱스/메타 구축
|
|
189
|
+
this._buildJointIndex(cloned);
|
|
190
|
+
// bounding box 계산 후 updateDimension으로 크기 반영
|
|
191
|
+
const box = new THREE.Box3().setFromObject(this.pivot);
|
|
192
|
+
this._objectSize = box.getSize(new THREE.Vector3());
|
|
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
|
+
}
|
|
228
|
+
// 초기 joint 상태 적용
|
|
229
|
+
const jointStates = this.component.state.joints;
|
|
230
|
+
if (jointStates) {
|
|
231
|
+
this._dispatchJointStates(jointStates);
|
|
232
|
+
}
|
|
233
|
+
// 자동 애니메이션 시작 (state.autoAnimate === 'sine'인 경우)
|
|
234
|
+
if (this.component.state.autoAnimate === 'sine') {
|
|
235
|
+
this._startAutoAnimate();
|
|
236
|
+
}
|
|
237
|
+
// 탑뷰 스냅샷
|
|
238
|
+
const source = this.component.state.src;
|
|
239
|
+
if (source && !RealObjectURDF._topViewCache.has(source)) {
|
|
240
|
+
try {
|
|
241
|
+
const canvas = RealObjectGLTF.renderTopView(this.pivot);
|
|
242
|
+
RealObjectURDF._topViewCache.set(source, canvas);
|
|
243
|
+
this.component._topViewSnapshot = canvas;
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// 스냅샷 실패는 치명적이지 않음
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (source) {
|
|
250
|
+
;
|
|
251
|
+
this.component._topViewSnapshot = RealObjectURDF._topViewCache.get(source);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
_resolveUnitScale() {
|
|
255
|
+
const s = this.component.state.unitScale;
|
|
256
|
+
if (typeof s === 'number' && s > 0)
|
|
257
|
+
return s;
|
|
258
|
+
// 기본: URDF meter → 에디터 mm 스케일 1000
|
|
259
|
+
return 1000;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* 조인트 트리를 순회하며 인덱스와 메타를 구축한다.
|
|
263
|
+
* fixed 조인트는 조작 대상이 아니므로 메타에서 제외하되 인덱스에는 남긴다.
|
|
264
|
+
*/
|
|
265
|
+
_buildJointIndex(robot) {
|
|
266
|
+
this._jointIndex.clear();
|
|
267
|
+
this._jointMeta.clear();
|
|
268
|
+
this._jointOriginals.clear();
|
|
269
|
+
const joints = robot.joints || {};
|
|
270
|
+
for (const [name, joint] of Object.entries(joints)) {
|
|
271
|
+
this._jointIndex.set(name, joint);
|
|
272
|
+
this._jointOriginals.set(name, joint.angle || 0);
|
|
273
|
+
if (joint.jointType === 'fixed')
|
|
274
|
+
continue;
|
|
275
|
+
this._jointMeta.set(name, {
|
|
276
|
+
name,
|
|
277
|
+
type: joint.jointType,
|
|
278
|
+
axis: { x: joint.axis.x, y: joint.axis.y, z: joint.axis.z },
|
|
279
|
+
limit: { ...joint.limit }
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
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 모드).
|
|
356
|
+
* 값은 숫자 또는 { value: number } 형식 모두 허용 (유연성).
|
|
357
|
+
*
|
|
358
|
+
* joint transform 변경 후 component.invalidate()로 재렌더를 요청한다.
|
|
359
|
+
* 표준 경로: invalidate → Layer.throttle_render → rAF __draw__ → trigger('redraw')
|
|
360
|
+
* → ThreeCapability.animate → renderThreeD.
|
|
361
|
+
*/
|
|
362
|
+
_applyJointStates(states) {
|
|
363
|
+
if (!this._robot)
|
|
364
|
+
return;
|
|
365
|
+
let changed = false;
|
|
366
|
+
for (const [name, raw] of Object.entries(states)) {
|
|
367
|
+
const joint = this._jointIndex.get(name);
|
|
368
|
+
if (!joint || joint.jointType === 'fixed')
|
|
369
|
+
continue;
|
|
370
|
+
const value = typeof raw === 'number' ? raw : raw?.value;
|
|
371
|
+
if (typeof value !== 'number' || !isFinite(value))
|
|
372
|
+
continue;
|
|
373
|
+
joint.setJointValue(value);
|
|
374
|
+
changed = true;
|
|
375
|
+
}
|
|
376
|
+
if (changed) {
|
|
377
|
+
this.component?.invalidate?.();
|
|
378
|
+
}
|
|
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
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* 모든 조인트를 원본 값으로 복원한다.
|
|
433
|
+
*/
|
|
434
|
+
_resetAllJoints() {
|
|
435
|
+
if (!this._robot)
|
|
436
|
+
return;
|
|
437
|
+
for (const [name, orig] of this._jointOriginals) {
|
|
438
|
+
const joint = this._jointIndex.get(name);
|
|
439
|
+
if (joint && joint.jointType !== 'fixed') {
|
|
440
|
+
joint.setJointValue(orig);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// --- 라이프사이클 ---
|
|
445
|
+
clear() {
|
|
446
|
+
this._stopAutoAnimate();
|
|
447
|
+
this._disposeController();
|
|
448
|
+
this._jointIndex.clear();
|
|
449
|
+
this._jointMeta.clear();
|
|
450
|
+
this._jointOriginals.clear();
|
|
451
|
+
this._robot = undefined;
|
|
452
|
+
return super.clear();
|
|
453
|
+
}
|
|
454
|
+
updateDimension() {
|
|
455
|
+
if (!this.pivot)
|
|
456
|
+
return;
|
|
457
|
+
const { width = 1, height = 1, depth = 1 } = this.component.state;
|
|
458
|
+
const { x = 1, y = 1, z = 1 } = this._objectSize || {};
|
|
459
|
+
// URDF는 관절 기반 로봇의 비율을 유지해야 하므로 uniform scale을 적용한다.
|
|
460
|
+
// 세 축 각각의 비율 중 가장 작은 값(best-fit)을 사용해 로봇이 컴포넌트의
|
|
461
|
+
// bounding box 안에 왜곡 없이 맞춰지도록 한다.
|
|
462
|
+
const unit = this._resolveUnitScale();
|
|
463
|
+
const baseX = x || 1;
|
|
464
|
+
const baseY = y || 1;
|
|
465
|
+
const baseZ = z || 1;
|
|
466
|
+
const upAxis = this.component.state.upAxis;
|
|
467
|
+
const rotated = upAxis !== 'y';
|
|
468
|
+
const rX = width / baseX;
|
|
469
|
+
const rY = (rotated ? depth : height) / baseY;
|
|
470
|
+
const rZ = (rotated ? height : depth) / baseZ;
|
|
471
|
+
const ratio = Math.min(rX, rY, rZ);
|
|
472
|
+
this.pivot.scale.setScalar(ratio * unit);
|
|
473
|
+
this.component.invalidate();
|
|
474
|
+
}
|
|
475
|
+
onchange(after, before) {
|
|
476
|
+
super.onchange(after, before);
|
|
477
|
+
if ('src' in after) {
|
|
478
|
+
this.updateSource();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if ('joints' in after) {
|
|
482
|
+
// 사용자가 joint를 직접 조작하면 자동 애니메이션이 이를 즉시 덮어써
|
|
483
|
+
// 의도를 상쇄하므로, 자동 애니메이션을 우선 중지한다.
|
|
484
|
+
if (this._animRaf != null) {
|
|
485
|
+
this._stopAutoAnimate();
|
|
486
|
+
this._component.setState({ autoAnimate: 'none' });
|
|
487
|
+
}
|
|
488
|
+
const jointStates = after.joints;
|
|
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();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if ('upAxis' in after || 'unitScale' in after || 'packages' in after) {
|
|
522
|
+
// 좌표/스케일/패키지 매핑 변경은 pivot 재구성이 필요하므로 전체 재빌드
|
|
523
|
+
this.build();
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
//# sourceMappingURL=real-object-urdf.js.map
|