@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.
package/TODO.md ADDED
@@ -0,0 +1,58 @@
1
+ # @operato/scene-urdf — TODO
2
+
3
+ ## 물리 시뮬레이션 (Physics)
4
+
5
+ URDF는 `<inertial>`(질량/관성), `<collision>`(충돌 기하), `<joint limit/dynamics>`
6
+ (effort, velocity, friction, damping), `<mimicJoint>` 등 물리 시뮬레이션에 필요한
7
+ 정보를 이미 품고 있다. 현재는 시각(visual) 부분만 사용.
8
+
9
+ ### 레벨별 접근
10
+
11
+ - **Level 1 — 관절 스무딩**
12
+ - 목표값 → 현재값 ODE 기반 감쇠 보간
13
+ - 라이브러리 불필요, 자체 구현
14
+ - 중력/충돌 없음 — 단순 모션 부드러움만 추가
15
+
16
+ - **Level 2 — Rapier.js 리지드바디 (권장 정식 해법)**
17
+ - [Rapier3D](https://rapier.rs/) WASM 통합
18
+ - URDF link/joint/inertia → Rapier world 매핑
19
+ - 관절 타입 1:1 매핑 (revolute/prismatic/fixed/spherical)
20
+ - 중력, 충돌, 마찰 자연스러움
21
+ - 다른 씬 오브젝트와 충돌 (floor/컨테이너/다른 로봇)
22
+
23
+ - **Level 3 — 씬 전역 물리 시뮬레이션**
24
+ - things-scene의 모든 3D 컴포넌트를 물리 월드로 편입
25
+ - 공장 전역 pick & place, 파레트 쌓기, 컨베이어 물류
26
+ - things-scene 엔진 수준의 수정 필요
27
+
28
+ ### 활성화 트리거
29
+
30
+ - `state.physics = 'none' | 'smooth' | 'rigid'` 프로퍼티
31
+ - `'smooth'`: Level 1
32
+ - `'rigid'`: Level 2 (Rapier)
33
+ - `'none'`(기본): 현재처럼 순수 kinematic
34
+
35
+ ### 유스케이스 매핑
36
+
37
+ | 시나리오 | 필요 레벨 |
38
+ |---|---|
39
+ | 센서 → joint 시각화 (현재 Phase B) | Level 0 (현재) |
40
+ | 자연스러운 움직임 (UX 개선) | Level 1 |
41
+ | pick & place 시뮬레이션 | Level 2 |
42
+ | 사전 프로그래밍 검증 | Level 2 ~ 3 |
43
+ | 사고/충돌 예방 시뮬레이션 | Level 3 |
44
+
45
+ ---
46
+
47
+ ## 기타 후속 과제
48
+
49
+ - [ ] xacro-parser 재도입 (ros-industrial 로봇 직접 로드)
50
+ - [ ] `package://` 다중 매핑 preset UI 개선 (URL 편집 가능하게)
51
+ - [ ] 번들 에셋(`assets/urdf-packages/dsr_description2/`)의 `package://` 해석
52
+ 자동 설정 preset 추가 (Doosan DSR 16종)
53
+ - [ ] 조인트별 커스텀 auto-animate 파라미터 (state.animate[joint] = {freq,amp,phase})
54
+ - [ ] 조인트 드래그 gizmo (3D 상에서 직접 관절 조작)
55
+ - [ ] IK (inverse kinematics) — end-effector drag로 관절 역산
56
+ - [ ] Trajectory 녹화/재생
57
+ - [ ] KHR_materials_variants 스타일 상태별 재질 swap
58
+ - [ ] ColladaLoader의 Z-UP 경고 suppress (선택)
@@ -1,4 +1,5 @@
1
1
  import './property-editor-urdf-joints.js';
2
+ import './property-editor-urdf-preset.js';
2
3
  declare const _default: {
3
4
  type: string;
4
5
  element: string;
@@ -1,8 +1,13 @@
1
1
  import './property-editor-urdf-joints.js';
2
+ import './property-editor-urdf-preset.js';
2
3
  export default [
3
4
  {
4
5
  type: 'urdf-joints',
5
6
  element: 'property-editor-urdf-joints'
7
+ },
8
+ {
9
+ type: 'urdf-preset',
10
+ element: 'property-editor-urdf-preset'
6
11
  }
7
12
  ];
8
13
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/editors/index.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAA;AAEzC,eAAe;IACb;QACE,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,6BAA6B;KACvC;CACF,CAAA","sourcesContent":["import './property-editor-urdf-joints.js'\n\nexport default [\n {\n type: 'urdf-joints',\n element: 'property-editor-urdf-joints'\n }\n]\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/editors/index.ts"],"names":[],"mappings":"AAAA,OAAO,kCAAkC,CAAA;AACzC,OAAO,kCAAkC,CAAA;AAEzC,eAAe;IACb;QACE,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,6BAA6B;KACvC;IACD;QACE,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,6BAA6B;KACvC;CACF,CAAA","sourcesContent":["import './property-editor-urdf-joints.js'\nimport './property-editor-urdf-preset.js'\n\nexport default [\n {\n type: 'urdf-joints',\n element: 'property-editor-urdf-joints'\n },\n {\n type: 'urdf-preset',\n element: 'property-editor-urdf-preset'\n }\n]\n"]}
@@ -103,14 +103,18 @@ let URDFJointsEditor = class URDFJointsEditor extends OxPropertyEditor {
103
103
  const joints = (value || {});
104
104
  return html `
105
105
  <fieldset fullwidth>
106
- <legend>Joints</legend>
106
+ <legend><ox-i18n msgid="label.joints">joints</ox-i18n></legend>
107
107
  <div joints-panel>
108
108
  ${this.meta.length === 0
109
- ? html `<div class="joint-waiting">Waiting for URDF to load…</div>`
109
+ ? html `<div class="joint-waiting">
110
+ <ox-i18n msgid="label.waiting-for-urdf-load">waiting for URDF to load…</ox-i18n>
111
+ </div>`
110
112
  : this.meta.map(m => this._renderJointSlider(m, joints))}
111
113
  </div>
112
114
  ${this.meta.length > 0
113
- ? html `<button class="reset-btn" @click=${this._onReset}>Reset all</button>`
115
+ ? html `<button class="reset-btn" @click=${this._onReset}>
116
+ <ox-i18n msgid="label.reset-all">reset all</ox-i18n>
117
+ </button>`
114
118
  : null}
115
119
  </fieldset>
116
120
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"property-editor-urdf-joints.js","sourceRoot":"","sources":["../../src/editors/property-editor-urdf-joints.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/i18n/ox-i18n.js'\n\nimport { css, html, TemplateResult } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { OxPropertyEditor, PropertySpec } from '@operato/property-editor'\nimport { ScrollbarStyles } from '@operato/styles/scrollbar-styles.js'\n\ntype JointMeta = {\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 RAD2DEG = 180 / Math.PI\n\n/**\n * URDF 조인트 슬라이더 에디터.\n *\n * 값(value)은 state.joints 객체: `{ [jointName]: number | { value: number } }`.\n * 조인트 메타(이름/타입/axis/limit)는 로드된 RealObjectURDF.jointMeta에서 읽으며,\n * 선택된 컴포넌트 참조를 얻기 위해 `i-need-selected` 커스텀 이벤트를 사용한다.\n *\n * 슬라이더 조작 시 state.joints를 immutable하게 업데이트한 새 객체를 value로\n * 설정 — things-scene setState의 deep-equality 변화 감지를 통과시키기 위함.\n */\n@customElement('property-editor-urdf-joints')\nexport default class URDFJointsEditor extends OxPropertyEditor {\n static styles = [\n ...OxPropertyEditor.styles,\n ScrollbarStyles,\n css`\n [joints-panel] {\n max-height: 320px;\n overflow-y: auto;\n padding: 4px 0;\n }\n\n .joint-row {\n display: grid;\n grid-template-columns: 80px 1fr 60px;\n gap: 6px;\n align-items: center;\n margin: 3px 0;\n }\n\n .joint-row > .joint-name {\n font-size: 11px;\n color: var(--md-sys-color-on-surface, #1c1b1f);\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .joint-row > .joint-name[data-fixed] {\n color: var(--md-sys-color-on-surface-variant, #888);\n font-style: italic;\n }\n\n .joint-row > input[type='range'] {\n width: 100%;\n accent-color: var(--md-sys-color-primary, #6750a4);\n }\n\n .joint-row > .joint-value {\n font: 11px monospace;\n color: var(--md-sys-color-on-surface-variant, #666);\n text-align: right;\n }\n\n .joint-empty,\n .joint-waiting {\n font-size: 11px;\n color: var(--md-sys-color-on-surface-variant, #888);\n padding: 8px 10px;\n font-style: italic;\n }\n\n .joint-type-badge {\n display: inline-block;\n font-size: 9px;\n padding: 1px 5px;\n margin-left: 4px;\n border-radius: 6px;\n background: var(--md-sys-color-secondary-container, #e8def8);\n color: var(--md-sys-color-on-secondary-container, #1d192b);\n text-transform: uppercase;\n }\n\n .reset-btn {\n font-size: 10px;\n padding: 3px 8px;\n margin-top: 4px;\n border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.2));\n border-radius: 6px;\n background: var(--md-sys-color-surface-container, #f3edf7);\n cursor: pointer;\n color: var(--md-sys-color-on-surface, #1c1b1f);\n }\n .reset-btn:hover {\n background: var(--md-sys-color-surface-container-highest, #e6e0e9);\n }\n `\n ]\n\n private _component: any = null\n private _pollTimer?: number\n\n @state() declare meta: JointMeta[]\n\n constructor() {\n super()\n this.meta = []\n }\n\n editorTemplate(value: any, _spec: PropertySpec): TemplateResult {\n const joints = (value || {}) as Record<string, number | { value?: number }>\n\n return html`\n <fieldset fullwidth>\n <legend>Joints</legend>\n <div joints-panel>\n ${this.meta.length === 0\n ? html`<div class=\"joint-waiting\">Waiting for URDF to load…</div>`\n : this.meta.map(m => this._renderJointSlider(m, joints))}\n </div>\n ${this.meta.length > 0\n ? html`<button class=\"reset-btn\" @click=${this._onReset}>Reset all</button>`\n : null}\n </fieldset>\n `\n }\n\n private _renderJointSlider(m: JointMeta, joints: Record<string, number | { value?: number }>) {\n const raw = joints[m.name]\n const internal = typeof raw === 'number' ? raw : (raw?.value ?? 0)\n const isRad = m.type === 'revolute' || m.type === 'continuous'\n const scale = isRad ? RAD2DEG : 1\n const unit = isRad ? '°' : 'm'\n const minDisp = m.limit.lower * scale\n const maxDisp = m.limit.upper * scale\n const dispVal = internal * scale\n const step = isRad ? 1 : Math.max((m.limit.upper - m.limit.lower) / 100, 0.001)\n\n return html`\n <div class=\"joint-row\">\n <span class=\"joint-name\" title=\"${m.name} (${m.type})\">\n ${m.name}\n <span class=\"joint-type-badge\">${m.type.substring(0, 3)}</span>\n </span>\n <input\n type=\"range\"\n min=${minDisp}\n max=${maxDisp}\n step=${step}\n .value=${String(dispVal)}\n data-joint=${m.name}\n data-scale=${scale}\n @input=${this._onSlide}\n @change=${this._stopChange}\n />\n <span class=\"joint-value\">${dispVal.toFixed(isRad ? 1 : 3)}${unit}</span>\n </div>\n `\n }\n\n private _onSlide = (e: Event) => {\n e.stopPropagation()\n const input = e.target as HTMLInputElement\n const jointName = input.dataset.joint!\n const scale = parseFloat(input.dataset.scale || '1')\n const internal = parseFloat(input.value) / scale\n\n // 이전 값을 immutable하게 새 객체로 머지. state.joints를 mutate하면\n // setState의 deep-equality 체크에서 before=after로 판정되어 change 감지\n // 실패 → onchange가 fire되지 않음. Phase 1에서 확인된 규칙.\n const prev = (this.value || {}) as Record<string, any>\n const next = { ...prev, [jointName]: internal }\n this.value = next\n\n this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }))\n }\n\n /**\n * 슬라이더 드래그 종료 시 브라우저가 네이티브 'change' 이벤트를 발행한다.\n * 이 이벤트가 OxPropertyEditor 베이스의 _valueChanged(renderRoot 리스너)에\n * 도달하면 input.value(문자열)를 this.value에 assign해버려서 joints 객체가\n * \"45\" 같은 문자열로 덮어쓰여진다. shadow DOM 내부에서 바로 stopPropagation.\n */\n private _stopChange = (e: Event) => {\n e.stopPropagation()\n }\n\n private _onReset = (e: MouseEvent) => {\n e.stopPropagation()\n this.value = {}\n this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }))\n }\n\n connectedCallback() {\n super.connectedCallback()\n this._requestSelectedComponent()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n if (this._pollTimer) {\n clearInterval(this._pollTimer)\n this._pollTimer = undefined\n }\n }\n\n /**\n * 선택된 URDF 컴포넌트를 얻어 jointMeta를 읽는다. 로드가 아직이면 주기적 폴링.\n */\n private _requestSelectedComponent() {\n this.dispatchEvent(\n new CustomEvent('i-need-selected', {\n bubbles: true,\n composed: true,\n detail: {\n callback: (selected: any[]) => {\n const component = selected?.[0]\n if (!component) return\n this._component = component\n this._refreshJointMeta()\n }\n }\n })\n )\n }\n\n private _refreshJointMeta() {\n const ro = this._component?.realObject\n const meta: JointMeta[] | undefined = ro?.jointMeta\n if (meta && meta.length > 0) {\n this.meta = meta\n if (this._pollTimer) {\n clearInterval(this._pollTimer)\n this._pollTimer = undefined\n }\n return\n }\n\n // 아직 로드 전이면 폴링 (최대 20회 = 10초)\n if (this._pollTimer) return\n let retries = 0\n this._pollTimer = window.setInterval(() => {\n const m = this._component?.realObject?.jointMeta\n if (m && m.length > 0) {\n this.meta = m\n clearInterval(this._pollTimer!)\n this._pollTimer = undefined\n } else if (++retries >= 20) {\n clearInterval(this._pollTimer!)\n this._pollTimer = undefined\n }\n }, 500)\n }\n}\n"]}
1
+ {"version":3,"file":"property-editor-urdf-joints.js","sourceRoot":"","sources":["../../src/editors/property-editor-urdf-joints.ts"],"names":[],"mappings":"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","sourcesContent":["import '@operato/i18n/ox-i18n.js'\n\nimport { css, html, TemplateResult } from 'lit'\nimport { customElement, state } from 'lit/decorators.js'\n\nimport { OxPropertyEditor, PropertySpec } from '@operato/property-editor'\nimport { ScrollbarStyles } from '@operato/styles/scrollbar-styles.js'\n\ntype JointMeta = {\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 RAD2DEG = 180 / Math.PI\n\n/**\n * URDF 조인트 슬라이더 에디터.\n *\n * 값(value)은 state.joints 객체: `{ [jointName]: number | { value: number } }`.\n * 조인트 메타(이름/타입/axis/limit)는 로드된 RealObjectURDF.jointMeta에서 읽으며,\n * 선택된 컴포넌트 참조를 얻기 위해 `i-need-selected` 커스텀 이벤트를 사용한다.\n *\n * 슬라이더 조작 시 state.joints를 immutable하게 업데이트한 새 객체를 value로\n * 설정 — things-scene setState의 deep-equality 변화 감지를 통과시키기 위함.\n */\n@customElement('property-editor-urdf-joints')\nexport default class URDFJointsEditor extends OxPropertyEditor {\n static styles = [\n ...OxPropertyEditor.styles,\n ScrollbarStyles,\n css`\n [joints-panel] {\n max-height: 320px;\n overflow-y: auto;\n padding: 4px 0;\n }\n\n .joint-row {\n display: grid;\n grid-template-columns: 80px 1fr 60px;\n gap: 6px;\n align-items: center;\n margin: 3px 0;\n }\n\n .joint-row > .joint-name {\n font-size: 11px;\n color: var(--md-sys-color-on-surface, #1c1b1f);\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n .joint-row > .joint-name[data-fixed] {\n color: var(--md-sys-color-on-surface-variant, #888);\n font-style: italic;\n }\n\n .joint-row > input[type='range'] {\n width: 100%;\n accent-color: var(--md-sys-color-primary, #6750a4);\n }\n\n .joint-row > .joint-value {\n font: 11px monospace;\n color: var(--md-sys-color-on-surface-variant, #666);\n text-align: right;\n }\n\n .joint-empty,\n .joint-waiting {\n font-size: 11px;\n color: var(--md-sys-color-on-surface-variant, #888);\n padding: 8px 10px;\n font-style: italic;\n }\n\n .joint-type-badge {\n display: inline-block;\n font-size: 9px;\n padding: 1px 5px;\n margin-left: 4px;\n border-radius: 6px;\n background: var(--md-sys-color-secondary-container, #e8def8);\n color: var(--md-sys-color-on-secondary-container, #1d192b);\n text-transform: uppercase;\n }\n\n .reset-btn {\n font-size: 10px;\n padding: 3px 8px;\n margin-top: 4px;\n border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.2));\n border-radius: 6px;\n background: var(--md-sys-color-surface-container, #f3edf7);\n cursor: pointer;\n color: var(--md-sys-color-on-surface, #1c1b1f);\n }\n .reset-btn:hover {\n background: var(--md-sys-color-surface-container-highest, #e6e0e9);\n }\n `\n ]\n\n private _component: any = null\n private _pollTimer?: number\n\n @state() declare meta: JointMeta[]\n\n constructor() {\n super()\n this.meta = []\n }\n\n editorTemplate(value: any, _spec: PropertySpec): TemplateResult {\n const joints = (value || {}) as Record<string, number | { value?: number }>\n\n return html`\n <fieldset fullwidth>\n <legend><ox-i18n msgid=\"label.joints\">joints</ox-i18n></legend>\n <div joints-panel>\n ${this.meta.length === 0\n ? html`<div class=\"joint-waiting\">\n <ox-i18n msgid=\"label.waiting-for-urdf-load\">waiting for URDF to load…</ox-i18n>\n </div>`\n : this.meta.map(m => this._renderJointSlider(m, joints))}\n </div>\n ${this.meta.length > 0\n ? html`<button class=\"reset-btn\" @click=${this._onReset}>\n <ox-i18n msgid=\"label.reset-all\">reset all</ox-i18n>\n </button>`\n : null}\n </fieldset>\n `\n }\n\n private _renderJointSlider(m: JointMeta, joints: Record<string, number | { value?: number }>) {\n const raw = joints[m.name]\n const internal = typeof raw === 'number' ? raw : (raw?.value ?? 0)\n const isRad = m.type === 'revolute' || m.type === 'continuous'\n const scale = isRad ? RAD2DEG : 1\n const unit = isRad ? '°' : 'm'\n const minDisp = m.limit.lower * scale\n const maxDisp = m.limit.upper * scale\n const dispVal = internal * scale\n const step = isRad ? 1 : Math.max((m.limit.upper - m.limit.lower) / 100, 0.001)\n\n return html`\n <div class=\"joint-row\">\n <span class=\"joint-name\" title=\"${m.name} (${m.type})\">\n ${m.name}\n <span class=\"joint-type-badge\">${m.type.substring(0, 3)}</span>\n </span>\n <input\n type=\"range\"\n min=${minDisp}\n max=${maxDisp}\n step=${step}\n .value=${String(dispVal)}\n data-joint=${m.name}\n data-scale=${scale}\n @input=${this._onSlide}\n @change=${this._stopChange}\n />\n <span class=\"joint-value\">${dispVal.toFixed(isRad ? 1 : 3)}${unit}</span>\n </div>\n `\n }\n\n private _onSlide = (e: Event) => {\n e.stopPropagation()\n const input = e.target as HTMLInputElement\n const jointName = input.dataset.joint!\n const scale = parseFloat(input.dataset.scale || '1')\n const internal = parseFloat(input.value) / scale\n\n // 이전 값을 immutable하게 새 객체로 머지. state.joints를 mutate하면\n // setState의 deep-equality 체크에서 before=after로 판정되어 change 감지\n // 실패 → onchange가 fire되지 않음. Phase 1에서 확인된 규칙.\n const prev = (this.value || {}) as Record<string, any>\n const next = { ...prev, [jointName]: internal }\n this.value = next\n\n this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }))\n }\n\n /**\n * 슬라이더 드래그 종료 시 브라우저가 네이티브 'change' 이벤트를 발행한다.\n * 이 이벤트가 OxPropertyEditor 베이스의 _valueChanged(renderRoot 리스너)에\n * 도달하면 input.value(문자열)를 this.value에 assign해버려서 joints 객체가\n * \"45\" 같은 문자열로 덮어쓰여진다. shadow DOM 내부에서 바로 stopPropagation.\n */\n private _stopChange = (e: Event) => {\n e.stopPropagation()\n }\n\n private _onReset = (e: MouseEvent) => {\n e.stopPropagation()\n this.value = {}\n this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }))\n }\n\n connectedCallback() {\n super.connectedCallback()\n this._requestSelectedComponent()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n if (this._pollTimer) {\n clearInterval(this._pollTimer)\n this._pollTimer = undefined\n }\n }\n\n /**\n * 선택된 URDF 컴포넌트를 얻어 jointMeta를 읽는다. 로드가 아직이면 주기적 폴링.\n */\n private _requestSelectedComponent() {\n this.dispatchEvent(\n new CustomEvent('i-need-selected', {\n bubbles: true,\n composed: true,\n detail: {\n callback: (selected: any[]) => {\n const component = selected?.[0]\n if (!component) return\n this._component = component\n this._refreshJointMeta()\n }\n }\n })\n )\n }\n\n private _refreshJointMeta() {\n const ro = this._component?.realObject\n const meta: JointMeta[] | undefined = ro?.jointMeta\n if (meta && meta.length > 0) {\n this.meta = meta\n if (this._pollTimer) {\n clearInterval(this._pollTimer)\n this._pollTimer = undefined\n }\n return\n }\n\n // 아직 로드 전이면 폴링 (최대 20회 = 10초)\n if (this._pollTimer) return\n let retries = 0\n this._pollTimer = window.setInterval(() => {\n const m = this._component?.realObject?.jointMeta\n if (m && m.length > 0) {\n this.meta = m\n clearInterval(this._pollTimer!)\n this._pollTimer = undefined\n } else if (++retries >= 20) {\n clearInterval(this._pollTimer!)\n this._pollTimer = undefined\n }\n }, 500)\n }\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import '@operato/i18n/ox-i18n.js';
2
+ import { TemplateResult } from 'lit';
3
+ import { OxPropertyEditor, PropertySpec } from '@operato/property-editor';
4
+ export default class URDFPresetEditor extends OxPropertyEditor {
5
+ static styles: import("lit").CSSResult[];
6
+ editorTemplate(value: any, _spec: PropertySpec): TemplateResult;
7
+ private _onSelect;
8
+ }
@@ -0,0 +1,114 @@
1
+ /*
2
+ * Copyright © HatioLab Inc. All rights reserved.
3
+ *
4
+ * URDF 프리셋 에디터.
5
+ * 드롭다운에서 프리셋 선택 시 컴포넌트의 src + 크기/축/스케일을 일괄 업데이트.
6
+ *
7
+ * 값: preset id ('custom' 또는 URDF_PRESETS[i].id).
8
+ * 변경 이벤트: i-need-selected로 컴포넌트에 set() 호출 → src/width/height/depth/
9
+ * upAxis/unitScale이 한 번에 propagate되어 부모의 undoableChange 한 덩어리로
10
+ * 묶임.
11
+ */
12
+ import { __decorate } from "tslib";
13
+ import '@operato/i18n/ox-i18n.js';
14
+ import { css, html } from 'lit';
15
+ import { customElement } from 'lit/decorators.js';
16
+ import { OxPropertyEditor } from '@operato/property-editor';
17
+ import { URDF_PRESETS, findPreset } from '../urdf-presets.js';
18
+ let URDFPresetEditor = class URDFPresetEditor extends OxPropertyEditor {
19
+ static styles = [
20
+ ...OxPropertyEditor.styles,
21
+ css `
22
+ select {
23
+ width: 100%;
24
+ padding: 4px 8px;
25
+ font-size: 12px;
26
+ border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.2));
27
+ border-radius: 4px;
28
+ background: var(--md-sys-color-surface, #fff);
29
+ color: var(--md-sys-color-on-surface, #1c1b1f);
30
+ }
31
+ optgroup {
32
+ font-style: normal;
33
+ font-weight: 600;
34
+ }
35
+ `
36
+ ];
37
+ editorTemplate(value, _spec) {
38
+ const current = typeof value === 'string' && value ? value : 'custom';
39
+ // 카테고리별 그룹핑
40
+ const byCategory = {};
41
+ for (const preset of URDF_PRESETS) {
42
+ ;
43
+ (byCategory[preset.category] ||= []).push(preset);
44
+ }
45
+ const categoryOrder = ['arm', 'mobile', 'aerial', 'humanoid', 'quadruped'];
46
+ const categoryLabel = {
47
+ arm: 'Manipulators',
48
+ mobile: 'Mobile Platforms',
49
+ aerial: 'Aerial',
50
+ humanoid: 'Humanoids',
51
+ quadruped: 'Legged'
52
+ };
53
+ return html `
54
+ <select id="editor" @change=${this._onSelect} .value=${current}>
55
+ <option value="custom" ?selected=${current === 'custom'}>Custom URL…</option>
56
+ ${categoryOrder.map(cat => {
57
+ const list = byCategory[cat];
58
+ if (!list || list.length === 0)
59
+ return null;
60
+ return html `
61
+ <optgroup label=${categoryLabel[cat]}>
62
+ ${list.map(p => html `<option value=${p.id} ?selected=${current === p.id}>${p.display}</option>`)}
63
+ </optgroup>
64
+ `;
65
+ })}
66
+ </select>
67
+ `;
68
+ }
69
+ _onSelect = (e) => {
70
+ e.stopPropagation();
71
+ const select = e.target;
72
+ const id = select.value;
73
+ this.value = id;
74
+ if (id === 'custom') {
75
+ // Custom 선택은 현재 src를 건드리지 않음 — 사용자가 직접 src 입력 필드에
76
+ // URL을 넣도록 둔다.
77
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
78
+ return;
79
+ }
80
+ const preset = findPreset(id);
81
+ if (!preset)
82
+ return;
83
+ // i-need-selected로 컴포넌트에 다중 속성 일괄 set.
84
+ // property-change 이벤트도 별도로 발행하여 preset 값 자체도 저장.
85
+ this.dispatchEvent(new CustomEvent('i-need-selected', {
86
+ bubbles: true,
87
+ composed: true,
88
+ detail: {
89
+ callback: (selected) => {
90
+ const component = selected?.[0];
91
+ if (!component)
92
+ return;
93
+ component.set({
94
+ preset: id,
95
+ src: preset.src,
96
+ packages: preset.packages ?? null,
97
+ width: preset.width,
98
+ height: preset.height,
99
+ depth: preset.depth,
100
+ upAxis: preset.upAxis,
101
+ unitScale: preset.unitScale
102
+ });
103
+ }
104
+ }
105
+ }));
106
+ // 자체 change 이벤트도 발행하여 preset 값이 state에 저장되도록
107
+ this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
108
+ };
109
+ };
110
+ URDFPresetEditor = __decorate([
111
+ customElement('property-editor-urdf-preset')
112
+ ], URDFPresetEditor);
113
+ export default URDFPresetEditor;
114
+ //# sourceMappingURL=property-editor-urdf-preset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"property-editor-urdf-preset.js","sourceRoot":"","sources":["../../src/editors/property-editor-urdf-preset.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;;AAEH,OAAO,0BAA0B,CAAA;AAEjC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAkB,MAAM,KAAK,CAAA;AAC/C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,gBAAgB,EAAgB,MAAM,0BAA0B,CAAA;AAEzE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAG9C,IAAM,gBAAgB,GAAtB,MAAM,gBAAiB,SAAQ,gBAAgB;IAC5D,MAAM,CAAC,MAAM,GAAG;QACd,GAAG,gBAAgB,CAAC,MAAM;QAC1B,GAAG,CAAA;;;;;;;;;;;;;;KAcF;KACF,CAAA;IAED,cAAc,CAAC,KAAU,EAAE,KAAmB;QAC5C,MAAM,OAAO,GAAG,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAA;QAErE,YAAY;QACZ,MAAM,UAAU,GAAwC,EAAE,CAAA;QAC1D,KAAK,MAAM,MAAM,IAAI,YAAY,EAAE,CAAC;YAClC,CAAC;YAAA,CAAC,UAAU,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACpD,CAAC;QACD,MAAM,aAAa,GAAmC,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,CAAC,CAAA;QAC1G,MAAM,aAAa,GAA2B;YAC5C,GAAG,EAAE,cAAc;YACnB,MAAM,EAAE,kBAAkB;YAC1B,MAAM,EAAE,QAAQ;YAChB,QAAQ,EAAE,WAAW;YACrB,SAAS,EAAE,QAAQ;SACpB,CAAA;QAED,OAAO,IAAI,CAAA;oCACqB,IAAI,CAAC,SAAS,WAAW,OAAO;2CACzB,OAAO,KAAK,QAAQ;UACrD,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;YACxB,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,CAAA;YAC5B,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,IAAI,CAAA;YAC3C,OAAO,IAAI,CAAA;8BACS,aAAa,CAAC,GAAG,CAAC;gBAChC,IAAI,CAAC,GAAG,CACR,CAAC,CAAC,EAAE,CAAC,IAAI,CAAA,iBAAiB,CAAC,CAAC,EAAE,cAAc,OAAO,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,OAAO,WAAW,CACrF;;WAEJ,CAAA;QACH,CAAC,CAAC;;KAEL,CAAA;IACH,CAAC;IAEO,SAAS,GAAG,CAAC,CAAQ,EAAE,EAAE;QAC/B,CAAC,CAAC,eAAe,EAAE,CAAA;QACnB,MAAM,MAAM,GAAG,CAAC,CAAC,MAA2B,CAAA;QAC5C,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAA;QAEvB,IAAI,CAAC,KAAK,GAAG,EAAE,CAAA;QAEf,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;YACpB,kDAAkD;YAClD,eAAe;YACf,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;YAChF,OAAM;QACR,CAAC;QAED,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,CAAC,CAAA;QAC7B,IAAI,CAAC,MAAM;YAAE,OAAM;QAEnB,uCAAuC;QACvC,iDAAiD;QACjD,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,iBAAiB,EAAE;YACjC,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,IAAI;YACd,MAAM,EAAE;gBACN,QAAQ,EAAE,CAAC,QAAe,EAAE,EAAE;oBAC5B,MAAM,SAAS,GAAG,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAA;oBAC/B,IAAI,CAAC,SAAS;wBAAE,OAAM;oBACtB,SAAS,CAAC,GAAG,CAAC;wBACZ,MAAM,EAAE,EAAE;wBACV,GAAG,EAAE,MAAM,CAAC,GAAG;wBACf,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,IAAI;wBACjC,KAAK,EAAE,MAAM,CAAC,KAAK;wBACnB,MAAM,EAAE,MAAM,CAAC,MAAM;wBACrB,KAAK,EAAE,MAAM,CAAC,KAAK;wBACnB,MAAM,EAAE,MAAM,CAAC,MAAM;wBACrB,SAAS,EAAE,MAAM,CAAC,SAAS;qBAC5B,CAAC,CAAA;gBACJ,CAAC;aACF;SACF,CAAC,CACH,CAAA;QAED,6CAA6C;QAC7C,IAAI,CAAC,aAAa,CAAC,IAAI,WAAW,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAClF,CAAC,CAAA;;AAnGkB,gBAAgB;IADpC,aAAa,CAAC,6BAA6B,CAAC;GACxB,gBAAgB,CAoGpC;eApGoB,gBAAgB","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * URDF 프리셋 에디터.\n * 드롭다운에서 프리셋 선택 시 컴포넌트의 src + 크기/축/스케일을 일괄 업데이트.\n *\n * 값: preset id ('custom' 또는 URDF_PRESETS[i].id).\n * 변경 이벤트: i-need-selected로 컴포넌트에 set() 호출 → src/width/height/depth/\n * upAxis/unitScale이 한 번에 propagate되어 부모의 undoableChange 한 덩어리로\n * 묶임.\n */\n\nimport '@operato/i18n/ox-i18n.js'\n\nimport { css, html, TemplateResult } from 'lit'\nimport { customElement } from 'lit/decorators.js'\n\nimport { OxPropertyEditor, PropertySpec } from '@operato/property-editor'\n\nimport { URDF_PRESETS, findPreset } from '../urdf-presets.js'\n\n@customElement('property-editor-urdf-preset')\nexport default class URDFPresetEditor extends OxPropertyEditor {\n static styles = [\n ...OxPropertyEditor.styles,\n css`\n select {\n width: 100%;\n padding: 4px 8px;\n font-size: 12px;\n border: 1px solid var(--md-sys-color-outline-variant, rgba(0, 0, 0, 0.2));\n border-radius: 4px;\n background: var(--md-sys-color-surface, #fff);\n color: var(--md-sys-color-on-surface, #1c1b1f);\n }\n optgroup {\n font-style: normal;\n font-weight: 600;\n }\n `\n ]\n\n editorTemplate(value: any, _spec: PropertySpec): TemplateResult {\n const current = typeof value === 'string' && value ? value : 'custom'\n\n // 카테고리별 그룹핑\n const byCategory: Record<string, typeof URDF_PRESETS> = {}\n for (const preset of URDF_PRESETS) {\n ;(byCategory[preset.category] ||= []).push(preset)\n }\n const categoryOrder: Array<keyof typeof byCategory> = ['arm', 'mobile', 'aerial', 'humanoid', 'quadruped']\n const categoryLabel: Record<string, string> = {\n arm: 'Manipulators',\n mobile: 'Mobile Platforms',\n aerial: 'Aerial',\n humanoid: 'Humanoids',\n quadruped: 'Legged'\n }\n\n return html`\n <select id=\"editor\" @change=${this._onSelect} .value=${current}>\n <option value=\"custom\" ?selected=${current === 'custom'}>Custom URL…</option>\n ${categoryOrder.map(cat => {\n const list = byCategory[cat]\n if (!list || list.length === 0) return null\n return html`\n <optgroup label=${categoryLabel[cat]}>\n ${list.map(\n p => html`<option value=${p.id} ?selected=${current === p.id}>${p.display}</option>`\n )}\n </optgroup>\n `\n })}\n </select>\n `\n }\n\n private _onSelect = (e: Event) => {\n e.stopPropagation()\n const select = e.target as HTMLSelectElement\n const id = select.value\n\n this.value = id\n\n if (id === 'custom') {\n // Custom 선택은 현재 src를 건드리지 않음 — 사용자가 직접 src 입력 필드에\n // URL을 넣도록 둔다.\n this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }))\n return\n }\n\n const preset = findPreset(id)\n if (!preset) return\n\n // i-need-selected로 컴포넌트에 다중 속성 일괄 set.\n // property-change 이벤트도 별도로 발행하여 preset 값 자체도 저장.\n this.dispatchEvent(\n new CustomEvent('i-need-selected', {\n bubbles: true,\n composed: true,\n detail: {\n callback: (selected: any[]) => {\n const component = selected?.[0]\n if (!component) return\n component.set({\n preset: id,\n src: preset.src,\n packages: preset.packages ?? null,\n width: preset.width,\n height: preset.height,\n depth: preset.depth,\n upAxis: preset.upAxis,\n unitScale: preset.unitScale\n })\n }\n }\n })\n )\n\n // 자체 change 이벤트도 발행하여 preset 값이 state에 저장되도록\n this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }))\n }\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import './urdf-object.js';
2
+ import './smoothing-controller.js';
2
3
  export { RealObjectURDF } from './real-object-urdf.js';
3
4
  export type { URDFJointState, URDFJointMeta } from './real-object-urdf.js';
4
5
  export { URDFObject } from './urdf-object.js';
6
+ export { registerJointController, createJointController, listJointControllers } from './joint-controller.js';
7
+ export type { JointController, JointControllerContext, JointControllerFactory } from './joint-controller.js';
8
+ export { SmoothingController } from './smoothing-controller.js';
package/dist/index.js CHANGED
@@ -12,6 +12,12 @@
12
12
  */
13
13
  // @sceneComponent decorator side-effect를 위해 import
14
14
  import './urdf-object.js';
15
+ // 기본 'smooth' controller 등록 (side-effect)
16
+ import './smoothing-controller.js';
15
17
  export { RealObjectURDF } from './real-object-urdf.js';
16
18
  export { URDFObject } from './urdf-object.js';
19
+ // 외부 모듈이 자체 JointController를 등록할 때 사용하는 공개 API.
20
+ // 예: @operato/scene-urdf-rapier가 `registerJointController('rapier', ...)`.
21
+ export { registerJointController, createJointController, listJointControllers } from './joint-controller.js';
22
+ export { SmoothingController } from './smoothing-controller.js';
17
23
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,mDAAmD;AACnD,OAAO,kBAAkB,CAAA;AAEzB,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAEtD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * URDF 플러그인 — things-scene의 RealObjectExternalModel 베이스를 상속받아\n * URDF(Unified Robot Description Format) 로봇을 네이티브 3D 씬 오브젝트로\n * 렌더링한다. @sceneComponent('urdf')로 등록되어 things-factory 보드에서\n * 일반 컴포넌트처럼 사용 가능.\n *\n * 기본 loader는 STL/DAE/GLB를 지원하며, GLB는 things-scene의 RealObjectGLTF\n * 캐시와 공유한다. xacro 전처리는 현재 미지원(ros-industrial 로봇 등은 사전\n * 변환된 URDF 필요).\n */\n\n// @sceneComponent decorator side-effect를 위해 import\nimport './urdf-object.js'\n\nexport { RealObjectURDF } from './real-object-urdf.js'\nexport type { URDFJointState, URDFJointMeta } from './real-object-urdf.js'\nexport { URDFObject } from './urdf-object.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,mDAAmD;AACnD,OAAO,kBAAkB,CAAA;AACzB,0CAA0C;AAC1C,OAAO,2BAA2B,CAAA;AAElC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAEtD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE7C,gDAAgD;AAChD,2EAA2E;AAC3E,OAAO,EACL,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,uBAAuB,CAAA;AAM9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAA","sourcesContent":["/*\n * Copyright © HatioLab Inc. All rights reserved.\n *\n * URDF 플러그인 — things-scene의 RealObjectExternalModel 베이스를 상속받아\n * URDF(Unified Robot Description Format) 로봇을 네이티브 3D 씬 오브젝트로\n * 렌더링한다. @sceneComponent('urdf')로 등록되어 things-factory 보드에서\n * 일반 컴포넌트처럼 사용 가능.\n *\n * 기본 loader는 STL/DAE/GLB를 지원하며, GLB는 things-scene의 RealObjectGLTF\n * 캐시와 공유한다. xacro 전처리는 현재 미지원(ros-industrial 로봇 등은 사전\n * 변환된 URDF 필요).\n */\n\n// @sceneComponent decorator side-effect를 위해 import\nimport './urdf-object.js'\n// 기본 'smooth' controller 등록 (side-effect)\nimport './smoothing-controller.js'\n\nexport { RealObjectURDF } from './real-object-urdf.js'\nexport type { URDFJointState, URDFJointMeta } from './real-object-urdf.js'\nexport { URDFObject } from './urdf-object.js'\n\n// 외부 모듈이 자체 JointController를 등록할 때 사용하는 공개 API.\n// 예: @operato/scene-urdf-rapier가 `registerJointController('rapier', ...)`.\nexport {\n registerJointController,\n createJointController,\n listJointControllers\n} from './joint-controller.js'\nexport type {\n JointController,\n JointControllerContext,\n JointControllerFactory\n} from './joint-controller.js'\nexport { SmoothingController } from './smoothing-controller.js'\n"]}
@@ -0,0 +1,35 @@
1
+ import type { URDFRobot, URDFJoint } from 'urdf-loader';
2
+ import type { URDFJointMeta } from './real-object-urdf.js';
3
+ export interface JointControllerContext {
4
+ robot: URDFRobot;
5
+ /** non-fixed + fixed 포함 모든 조인트 */
6
+ joints: Map<string, URDFJoint>;
7
+ /** non-fixed 조인트의 메타 (type, axis, limit) — fixed는 제외됨 */
8
+ jointMeta: Map<string, URDFJointMeta>;
9
+ }
10
+ export interface JointController {
11
+ /** 로봇 로드 직후 1회 호출. 내부 상태(current/target/velocity 등) 초기화. */
12
+ setup(ctx: JointControllerContext): void;
13
+ /**
14
+ * state.joints 변경 시 호출. controller가 `targets` 맵을 내부 저장하고
15
+ * tick에서 실제 적용을 결정한다. 각 value는 joint의 internal unit
16
+ * (revolute=rad, prismatic=m).
17
+ */
18
+ setTargets(targets: Record<string, number>): void;
19
+ /**
20
+ * 매 프레임 호출 (requestAnimationFrame). dt는 초 단위.
21
+ * 관절에 값을 적용(joint.setJointValue)하고, 값이 실제로 변경됐다면 true를
22
+ * 반환 (상위가 invalidate를 결정).
23
+ */
24
+ tick(dt: number): boolean;
25
+ /** 로봇 해제 시 호출. 내부 자원/캐시 정리. */
26
+ dispose(): void;
27
+ }
28
+ export type JointControllerFactory = () => JointController;
29
+ /**
30
+ * 외부 패키지가 자신의 controller 구현을 등록한다.
31
+ * 예: `registerJointController('rapier', () => new RapierJointController())`.
32
+ */
33
+ export declare function registerJointController(name: string, factory: JointControllerFactory): void;
34
+ export declare function createJointController(name: string): JointController | undefined;
35
+ export declare function listJointControllers(): string[];
@@ -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"]}
@@ -28,14 +28,17 @@ export declare class RealObjectURDF extends RealObjectExternalModel<URDFRobot> {
28
28
  private static _topViewCache;
29
29
  /**
30
30
  * URDF를 로드한다 (동일 URL 캐시).
31
- * mesh 로더는 STL/DAE(내장) + GLB(확장)를 지원한다.
31
+ * mesh 로더는 STL/DAE(내장) + OBJ(MTL 연동) + GLB(확장)를 지원한다.
32
32
  *
33
33
  * URDFLoader.loadAsync는 URDF XML 파싱 직후 resolve하지만 mesh(STL/DAE/GLB)는
34
34
  * 비동기로 뒤늦게 attach된다. robot.clone() 시점에 mesh가 아직 없으면 클론된
35
35
  * 트리엔 visual이 비게 되고 원본에만 mesh가 붙는다. 전용 LoadingManager의
36
36
  * onLoad를 사용해 URDF + 모든 mesh가 완료된 후 resolve한다.
37
+ *
38
+ * `packages` 옵션을 통해 ROS `package://<pkg>/<rel>` prefix 해석을 설정할 수
39
+ * 있다 (예: `{ a1_description: 'https://.../robots/a1_description' }`).
37
40
  */
38
- static loadURDF(url: string): Promise<URDFRobot>;
41
+ static loadURDF(url: string, packages?: Record<string, string>): Promise<URDFRobot>;
39
42
  /** 모든 캐시 비움 (보드 전환 시 호출) */
40
43
  static flushCache(): void;
41
44
  static getTopViewCache(source: string): HTMLCanvasElement | undefined;
@@ -44,6 +47,11 @@ export declare class RealObjectURDF extends RealObjectExternalModel<URDFRobot> {
44
47
  private _jointIndex;
45
48
  private _jointMeta;
46
49
  private _jointOriginals;
50
+ private _animRaf?;
51
+ private _animStartTime;
52
+ private _controller?;
53
+ private _ctrlRaf?;
54
+ private _ctrlLastTime;
47
55
  get robot(): URDFRobot | undefined;
48
56
  /** 로드된 조인트 메타 리스트 (property panel 등이 참조) */
49
57
  get jointMeta(): URDFJointMeta[];
@@ -57,7 +65,19 @@ export declare class RealObjectURDF extends RealObjectExternalModel<URDFRobot> {
57
65
  */
58
66
  private _buildJointIndex;
59
67
  /**
60
- * state.joints 맵을 순회하며 조인트 값을 적용한다.
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 모드).
61
81
  * 값은 숫자 또는 { value: number } 형식 모두 허용 (유연성).
62
82
  *
63
83
  * joint transform 변경 후 component.invalidate()로 재렌더를 요청한다.
@@ -65,6 +85,17 @@ export declare class RealObjectURDF extends RealObjectExternalModel<URDFRobot> {
65
85
  * → ThreeCapability.animate → renderThreeD.
66
86
  */
67
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;
68
99
  /**
69
100
  * 모든 조인트를 원본 값으로 복원한다.
70
101
  */