@operato/scene-urdf 1.2.2
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/CHANGELOG.md +11 -0
- package/README.md +41 -0
- package/assets/images/spinner.png +0 -0
- package/demo/index.html +109 -0
- package/dist/editors/index.d.ts +0 -0
- package/dist/editors/index.js +2 -0
- package/dist/editors/index.js.map +1 -0
- package/dist/elements/drag-n-drop.d.ts +2 -0
- package/dist/elements/drag-n-drop.js +126 -0
- package/dist/elements/drag-n-drop.js.map +1 -0
- package/dist/elements/urdf-controller-element.d.ts +12 -0
- package/dist/elements/urdf-controller-element.js +283 -0
- package/dist/elements/urdf-controller-element.js.map +1 -0
- package/dist/elements/urdf-drag-controls.d.ts +32 -0
- package/dist/elements/urdf-drag-controls.js +185 -0
- package/dist/elements/urdf-drag-controls.js.map +1 -0
- package/dist/elements/urdf-manipulator-element.d.ts +15 -0
- package/dist/elements/urdf-manipulator-element.js +110 -0
- package/dist/elements/urdf-manipulator-element.js.map +1 -0
- package/dist/elements/urdf-viewer-element.d.ts +53 -0
- package/dist/elements/urdf-viewer-element.js +401 -0
- package/dist/elements/urdf-viewer-element.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/templates/index.d.ts +14 -0
- package/dist/templates/index.js +4 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/urdf-controller.d.ts +14 -0
- package/dist/templates/urdf-controller.js +16 -0
- package/dist/templates/urdf-controller.js.map +1 -0
- package/dist/templates/urdf-viewer.d.ts +14 -0
- package/dist/templates/urdf-viewer.js +16 -0
- package/dist/templates/urdf-viewer.js.map +1 -0
- package/dist/urdf-controller.d.ts +27 -0
- package/dist/urdf-controller.js +65 -0
- package/dist/urdf-controller.js.map +1 -0
- package/dist/urdf-viewer.d.ts +50 -0
- package/dist/urdf-viewer.js +198 -0
- package/dist/urdf-viewer.js.map +1 -0
- package/icons/urdf-controller.png +0 -0
- package/icons/urdf-viewer.png +0 -0
- package/package.json +66 -0
- package/src/editors/index.ts +0 -0
- package/src/elements/drag-n-drop.ts +142 -0
- package/src/elements/urdf-controller-element.ts +297 -0
- package/src/elements/urdf-drag-controls.ts +248 -0
- package/src/elements/urdf-manipulator-element.ts +133 -0
- package/src/elements/urdf-viewer-element.ts +495 -0
- package/src/index.ts +2 -0
- package/src/templates/index.ts +4 -0
- package/src/templates/urdf-controller.ts +16 -0
- package/src/templates/urdf-viewer.ts +16 -0
- package/src/urdf-controller.ts +82 -0
- package/src/urdf-viewer.ts +249 -0
- package/things-scene.config.js +7 -0
- package/translations/en.json +16 -0
- package/translations/ko.json +16 -0
- package/translations/ms.json +16 -0
- package/translations/zh.json +16 -0
- package/tsconfig.json +22 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/web-dev-server.config.mjs +27 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { LitElement, css, html, PropertyValues } from 'lit'
|
|
2
|
+
import { customElement, property } from 'lit/decorators.js'
|
|
3
|
+
import * as THREE from 'three'
|
|
4
|
+
import { registerDragEvents } from './drag-n-drop'
|
|
5
|
+
import { ScrollbarStyles } from '@operato/styles'
|
|
6
|
+
import { URDFRobot } from 'urdf-loader'
|
|
7
|
+
import URDFManipulatorElement from './urdf-manipulator-element'
|
|
8
|
+
|
|
9
|
+
const DEG2RAD = Math.PI / 180
|
|
10
|
+
const RAD2DEG = 1 / DEG2RAD
|
|
11
|
+
|
|
12
|
+
@customElement('urdf-controller')
|
|
13
|
+
export default class URDFControllerElement extends LitElement {
|
|
14
|
+
static styles = [
|
|
15
|
+
ScrollbarStyles,
|
|
16
|
+
css`
|
|
17
|
+
:host {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
width: 100%;
|
|
21
|
+
margin: 15px 0;
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
user-select: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
:host > * {
|
|
27
|
+
margin: 5px 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
input[type='number'] {
|
|
31
|
+
color: white;
|
|
32
|
+
border: none;
|
|
33
|
+
font-weight: 300;
|
|
34
|
+
background: rgba(255, 255, 255, 0.25);
|
|
35
|
+
padding: 1px 2px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ul {
|
|
39
|
+
list-style: none;
|
|
40
|
+
padding: 0;
|
|
41
|
+
margin: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
input[type='range'] {
|
|
45
|
+
-webkit-appearance: none;
|
|
46
|
+
border: none;
|
|
47
|
+
outline: none;
|
|
48
|
+
width: 100%;
|
|
49
|
+
flex: 1;
|
|
50
|
+
height: 16px;
|
|
51
|
+
background-color: transparent;
|
|
52
|
+
}
|
|
53
|
+
input[type='range']::-webkit-slider-runnable-track {
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: 1px;
|
|
56
|
+
background: white;
|
|
57
|
+
border: none;
|
|
58
|
+
border-radius: 5px;
|
|
59
|
+
}
|
|
60
|
+
input[type='range']::-webkit-slider-thumb {
|
|
61
|
+
-webkit-appearance: none;
|
|
62
|
+
border: none;
|
|
63
|
+
height: 10px;
|
|
64
|
+
width: 10px;
|
|
65
|
+
border-radius: 50%;
|
|
66
|
+
background: white;
|
|
67
|
+
margin-top: -5px;
|
|
68
|
+
}
|
|
69
|
+
input[type='range']:focus {
|
|
70
|
+
outline: none;
|
|
71
|
+
}
|
|
72
|
+
input[type='range']:focus::-webkit-slider-runnable-track {
|
|
73
|
+
background: white;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
input[type='range']::-moz-range-track {
|
|
77
|
+
width: 100%;
|
|
78
|
+
height: 1px;
|
|
79
|
+
background: white;
|
|
80
|
+
border: none;
|
|
81
|
+
border-radius: 5px;
|
|
82
|
+
}
|
|
83
|
+
input[type='range']::-moz-range-thumb {
|
|
84
|
+
border: none;
|
|
85
|
+
height: 10px;
|
|
86
|
+
width: 10px;
|
|
87
|
+
border-radius: 50%;
|
|
88
|
+
background: white;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
input[type='range']:-moz-focusring {
|
|
92
|
+
outline: 1px solid white;
|
|
93
|
+
outline-offset: -1px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
input[type='range']::-ms-track {
|
|
97
|
+
width: 100%;
|
|
98
|
+
height: 1px;
|
|
99
|
+
background: white;
|
|
100
|
+
border-radius: 10px;
|
|
101
|
+
color: transparent;
|
|
102
|
+
border: none;
|
|
103
|
+
outline: none;
|
|
104
|
+
}
|
|
105
|
+
input[type='range']::-ms-thumb {
|
|
106
|
+
height: 10px;
|
|
107
|
+
width: 10px;
|
|
108
|
+
border-radius: 50%;
|
|
109
|
+
background: white;
|
|
110
|
+
border: none;
|
|
111
|
+
outline: none;
|
|
112
|
+
margin-top: 2px;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
input:focus {
|
|
116
|
+
outline: none;
|
|
117
|
+
opacity: 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* list of joint sliders */
|
|
121
|
+
ul {
|
|
122
|
+
flex: 1;
|
|
123
|
+
overflow-y: auto;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
li {
|
|
127
|
+
font-size: 16px;
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
padding: 1px 0;
|
|
131
|
+
|
|
132
|
+
width: 100%;
|
|
133
|
+
user-select: text;
|
|
134
|
+
|
|
135
|
+
transition: background 0.25s ease;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
li[robot-hovered] {
|
|
139
|
+
background: rgba(255, 255, 255, 0.35);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
li:hover {
|
|
143
|
+
background: rgba(255, 255, 255, 0.35);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
li span {
|
|
147
|
+
padding: 0 5px;
|
|
148
|
+
max-width: 125px;
|
|
149
|
+
text-overflow: ellipsis;
|
|
150
|
+
overflow: hidden;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
li input[type='number'] {
|
|
154
|
+
width: 50px;
|
|
155
|
+
overflow: hidden;
|
|
156
|
+
}
|
|
157
|
+
`
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
@property({ type: Object }) viewer?: URDFManipulatorElement
|
|
161
|
+
@property({ type: Object }) robot?: URDFRobot
|
|
162
|
+
|
|
163
|
+
render() {
|
|
164
|
+
const { joints = {} } = this.robot || {}
|
|
165
|
+
const { ignoreLimits = false } = this.viewer || {}
|
|
166
|
+
const jointsArray = Object.values(joints)
|
|
167
|
+
|
|
168
|
+
return html`
|
|
169
|
+
<ul>
|
|
170
|
+
${jointsArray.map(joint => {
|
|
171
|
+
var { jointType, name, angle, limit } = joint
|
|
172
|
+
var degVal: number | string = Number(angle || 0)
|
|
173
|
+
|
|
174
|
+
if (jointType === 'revolute' || jointType === 'continuous') {
|
|
175
|
+
degVal *= RAD2DEG
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (Math.abs(degVal) > 1) {
|
|
179
|
+
degVal = degVal.toFixed(1)
|
|
180
|
+
} else {
|
|
181
|
+
degVal = degVal.toPrecision(2)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
var sliderMin
|
|
185
|
+
var sliderMax
|
|
186
|
+
var inputMin
|
|
187
|
+
var inputMax
|
|
188
|
+
|
|
189
|
+
if (ignoreLimits || jointType === 'continuous') {
|
|
190
|
+
sliderMin = -6.28
|
|
191
|
+
sliderMax = 6.28
|
|
192
|
+
|
|
193
|
+
inputMin = -6.28 * RAD2DEG
|
|
194
|
+
inputMax = 6.28 * RAD2DEG
|
|
195
|
+
} else {
|
|
196
|
+
sliderMin = limit.lower
|
|
197
|
+
sliderMax = limit.upper
|
|
198
|
+
|
|
199
|
+
inputMin = Number(limit.lower) * RAD2DEG
|
|
200
|
+
inputMax = Number(limit.upper) * RAD2DEG
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return html`
|
|
204
|
+
<li joint-type=${jointType} joint-name=${name}>
|
|
205
|
+
<span title=${name}>${name}</span>
|
|
206
|
+
<input
|
|
207
|
+
type="range"
|
|
208
|
+
.value=${String(angle)}
|
|
209
|
+
step="0.0001"
|
|
210
|
+
min=${sliderMin}
|
|
211
|
+
max=${sliderMax}
|
|
212
|
+
@input=${(e: Event) => {
|
|
213
|
+
this.viewer?.setJointValue(name, (e.target as any).value)
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
216
|
+
<input
|
|
217
|
+
type="number"
|
|
218
|
+
.value=${degVal}
|
|
219
|
+
step="0.0001"
|
|
220
|
+
min=${inputMin}
|
|
221
|
+
max=${inputMax}
|
|
222
|
+
@change=${(e: Event) => {
|
|
223
|
+
this.viewer?.setJointValue(name, Number((e.target as HTMLInputElement).value) * DEG2RAD)
|
|
224
|
+
}}
|
|
225
|
+
/>
|
|
226
|
+
</li>
|
|
227
|
+
`
|
|
228
|
+
})}
|
|
229
|
+
</ul>
|
|
230
|
+
`
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
updated(changes: PropertyValues<this>) {
|
|
234
|
+
if (changes.has('viewer')) {
|
|
235
|
+
this._setupViewer(this.viewer)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
_setupViewer(viewer?: URDFManipulatorElement) {
|
|
240
|
+
if (!viewer) {
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
this.robot = viewer.robot
|
|
245
|
+
|
|
246
|
+
viewer.addEventListener('urdf-processed', () => {
|
|
247
|
+
this.robot = viewer.robot
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
viewer.addEventListener('ignore-limits-change', () => {
|
|
251
|
+
this.requestUpdate()
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
viewer.addEventListener('angle-change', (e: Event) => {
|
|
255
|
+
this.requestUpdate()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
viewer.addEventListener('joint-mouseover', (e: Event) => {
|
|
259
|
+
const j = this.renderRoot.querySelector(`li[joint-name="${(e as CustomEvent).detail}"]`)
|
|
260
|
+
if (j) {
|
|
261
|
+
j.setAttribute('robot-hovered', 'true')
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
viewer.addEventListener('joint-mouseout', (e: Event) => {
|
|
266
|
+
const j = this.renderRoot.querySelector(`li[joint-name="${(e as CustomEvent).detail}"]`)
|
|
267
|
+
if (j) {
|
|
268
|
+
j.removeAttribute('robot-hovered')
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
let originalNoAutoRecenter: boolean
|
|
273
|
+
viewer.addEventListener('manipulate-start', (e: Event) => {
|
|
274
|
+
const j = this.renderRoot.querySelector(`li[joint-name="${(e as CustomEvent).detail}"]`)
|
|
275
|
+
if (j) {
|
|
276
|
+
// ??
|
|
277
|
+
j.scrollIntoView({ block: 'nearest' })
|
|
278
|
+
window.scrollTo(0, 0)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
originalNoAutoRecenter = viewer.noAutoRecenter
|
|
282
|
+
viewer.noAutoRecenter = true
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
viewer.addEventListener('manipulate-end', (e: Event) => {
|
|
286
|
+
viewer.noAutoRecenter = originalNoAutoRecenter
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
registerDragEvents(viewer, () => {
|
|
290
|
+
this.setColor('#263238')
|
|
291
|
+
})
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
setColor(color: string) {
|
|
295
|
+
this.viewer!.highlightColor = '#' + new THREE.Color(0xffffff).lerp(new THREE.Color(color), 0.35).getHexString()
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { Raycaster, Vector3, Plane, Vector2, Scene, Camera, Object3D, Ray } from 'three'
|
|
2
|
+
import { URDFJoint } from 'urdf-loader'
|
|
3
|
+
|
|
4
|
+
// Find the nearest parent that is a joint
|
|
5
|
+
function isJoint(j: URDFJoint) {
|
|
6
|
+
return j.isURDFJoint && j.jointType !== 'fixed'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function findNearestJoint(child: Object3D) {
|
|
10
|
+
let curr: Object3D | null = child
|
|
11
|
+
while (curr) {
|
|
12
|
+
if (isJoint(curr as URDFJoint)) {
|
|
13
|
+
return curr
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
curr = curr.parent
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return curr
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const prevHitPoint = new Vector3()
|
|
23
|
+
const newHitPoint = new Vector3()
|
|
24
|
+
const pivotPoint = new Vector3()
|
|
25
|
+
const tempVector = new Vector3()
|
|
26
|
+
const tempVector2 = new Vector3()
|
|
27
|
+
const projectedStartPoint = new Vector3()
|
|
28
|
+
const projectedEndPoint = new Vector3()
|
|
29
|
+
const plane = new Plane()
|
|
30
|
+
export class URDFDragControls {
|
|
31
|
+
scene: Scene
|
|
32
|
+
|
|
33
|
+
enabled: boolean
|
|
34
|
+
raycaster: Raycaster
|
|
35
|
+
initialGrabPoint: Vector3
|
|
36
|
+
hitDistance: number
|
|
37
|
+
|
|
38
|
+
hovered: any
|
|
39
|
+
manipulating: any
|
|
40
|
+
|
|
41
|
+
constructor(scene: Scene) {
|
|
42
|
+
this.enabled = true
|
|
43
|
+
this.scene = scene
|
|
44
|
+
|
|
45
|
+
this.raycaster = new Raycaster()
|
|
46
|
+
this.initialGrabPoint = new Vector3()
|
|
47
|
+
|
|
48
|
+
this.hitDistance = -1
|
|
49
|
+
this.hovered = null
|
|
50
|
+
this.manipulating = null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
update() {
|
|
54
|
+
const { raycaster, hovered, manipulating, scene } = this
|
|
55
|
+
|
|
56
|
+
if (manipulating) {
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let hoveredJoint = null
|
|
61
|
+
const intersections = raycaster.intersectObject(scene, true)
|
|
62
|
+
if (intersections.length !== 0) {
|
|
63
|
+
const hit = intersections[0]
|
|
64
|
+
this.hitDistance = hit.distance
|
|
65
|
+
hoveredJoint = findNearestJoint(hit.object)
|
|
66
|
+
this.initialGrabPoint.copy(hit.point)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (hoveredJoint !== hovered) {
|
|
70
|
+
if (hovered) {
|
|
71
|
+
this.onUnhover(hovered)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.hovered = hoveredJoint
|
|
75
|
+
|
|
76
|
+
if (hoveredJoint) {
|
|
77
|
+
this.onHover(hoveredJoint as URDFJoint)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
updateJoint(joint: URDFJoint, angle: number) {
|
|
83
|
+
joint.setJointValue(angle)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onDragStart(joint: URDFJoint) {}
|
|
87
|
+
|
|
88
|
+
onDragEnd(joint: URDFJoint) {}
|
|
89
|
+
|
|
90
|
+
onHover(joint: URDFJoint) {}
|
|
91
|
+
|
|
92
|
+
onUnhover(joint: URDFJoint) {}
|
|
93
|
+
|
|
94
|
+
getRevoluteDelta(joint: URDFJoint, startPoint: Vector3, endPoint: Vector3) {
|
|
95
|
+
// set up the plane
|
|
96
|
+
tempVector.copy(joint.axis).transformDirection(joint.matrixWorld).normalize()
|
|
97
|
+
pivotPoint.set(0, 0, 0).applyMatrix4(joint.matrixWorld)
|
|
98
|
+
plane.setFromNormalAndCoplanarPoint(tempVector, pivotPoint)
|
|
99
|
+
|
|
100
|
+
// project the drag points onto the plane
|
|
101
|
+
plane.projectPoint(startPoint, projectedStartPoint)
|
|
102
|
+
plane.projectPoint(endPoint, projectedEndPoint)
|
|
103
|
+
|
|
104
|
+
// get the directions relative to the pivot
|
|
105
|
+
projectedStartPoint.sub(pivotPoint)
|
|
106
|
+
projectedEndPoint.sub(pivotPoint)
|
|
107
|
+
|
|
108
|
+
tempVector.crossVectors(projectedStartPoint, projectedEndPoint)
|
|
109
|
+
|
|
110
|
+
const direction = Math.sign(tempVector.dot(plane.normal))
|
|
111
|
+
return direction * projectedEndPoint.angleTo(projectedStartPoint)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
getPrismaticDelta(joint: URDFJoint, startPoint: Vector3, endPoint: Vector3) {
|
|
115
|
+
tempVector.subVectors(endPoint, startPoint)
|
|
116
|
+
plane.normal.copy(joint.axis).transformDirection(joint.parent!.matrixWorld).normalize()
|
|
117
|
+
|
|
118
|
+
return tempVector.dot(plane.normal)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
moveRay(toRay: Ray) {
|
|
122
|
+
const { raycaster, hitDistance, manipulating } = this
|
|
123
|
+
const { ray } = raycaster
|
|
124
|
+
|
|
125
|
+
if (manipulating) {
|
|
126
|
+
ray.at(hitDistance, prevHitPoint)
|
|
127
|
+
toRay.at(hitDistance, newHitPoint)
|
|
128
|
+
|
|
129
|
+
let delta = 0
|
|
130
|
+
if (manipulating.jointType === 'revolute' || manipulating.jointType === 'continuous') {
|
|
131
|
+
delta = this.getRevoluteDelta(manipulating, prevHitPoint, newHitPoint)
|
|
132
|
+
} else if (manipulating.jointType === 'prismatic') {
|
|
133
|
+
delta = this.getPrismaticDelta(manipulating, prevHitPoint, newHitPoint)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (delta) {
|
|
137
|
+
this.updateJoint(manipulating, manipulating.angle + delta)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.raycaster.ray.copy(toRay)
|
|
142
|
+
this.update()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
setGrabbed(grabbed: boolean) {
|
|
146
|
+
const { hovered, manipulating } = this
|
|
147
|
+
if (grabbed) {
|
|
148
|
+
if (manipulating !== null || hovered === null) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.manipulating = hovered
|
|
153
|
+
this.onDragStart(hovered)
|
|
154
|
+
} else {
|
|
155
|
+
if (this.manipulating === null) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
this.onDragEnd(this.manipulating)
|
|
160
|
+
this.manipulating = null
|
|
161
|
+
this.update()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export class PointerURDFDragControls extends URDFDragControls {
|
|
167
|
+
camera: Camera
|
|
168
|
+
domElement: HTMLElement
|
|
169
|
+
|
|
170
|
+
_mouseDown: (e: MouseEvent) => void
|
|
171
|
+
_mouseMove: (e: MouseEvent) => void
|
|
172
|
+
_mouseUp: (e: MouseEvent) => void
|
|
173
|
+
|
|
174
|
+
constructor(scene: Scene, camera: Camera, domElement: HTMLElement) {
|
|
175
|
+
super(scene)
|
|
176
|
+
|
|
177
|
+
this.camera = camera
|
|
178
|
+
this.domElement = domElement
|
|
179
|
+
|
|
180
|
+
const raycaster = new Raycaster()
|
|
181
|
+
const mouse = new Vector2()
|
|
182
|
+
|
|
183
|
+
function updateMouse(e: MouseEvent) {
|
|
184
|
+
mouse.x = (e.offsetX / domElement.offsetWidth) * 2 - 1
|
|
185
|
+
mouse.y = -(e.offsetY / domElement.offsetHeight) * 2 + 1
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this._mouseDown = (e: MouseEvent) => {
|
|
189
|
+
updateMouse(e)
|
|
190
|
+
raycaster.setFromCamera(mouse, this.camera)
|
|
191
|
+
this.moveRay(raycaster.ray)
|
|
192
|
+
this.setGrabbed(true)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this._mouseMove = (e: MouseEvent) => {
|
|
196
|
+
updateMouse(e)
|
|
197
|
+
raycaster.setFromCamera(mouse, this.camera)
|
|
198
|
+
this.moveRay(raycaster.ray)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this._mouseUp = (e: MouseEvent) => {
|
|
202
|
+
updateMouse(e)
|
|
203
|
+
raycaster.setFromCamera(mouse, this.camera)
|
|
204
|
+
this.moveRay(raycaster.ray)
|
|
205
|
+
this.setGrabbed(false)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
domElement.addEventListener('pointerdown', this._mouseDown)
|
|
209
|
+
domElement.addEventListener('pointermove', this._mouseMove)
|
|
210
|
+
domElement.addEventListener('pointerup', this._mouseUp)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
getRevoluteDelta(joint: URDFJoint, startPoint: Vector3, endPoint: Vector3) {
|
|
214
|
+
const { camera, initialGrabPoint } = this
|
|
215
|
+
|
|
216
|
+
// set up the plane
|
|
217
|
+
tempVector.copy(joint.axis).transformDirection(joint.matrixWorld).normalize()
|
|
218
|
+
pivotPoint.set(0, 0, 0).applyMatrix4(joint.matrixWorld)
|
|
219
|
+
plane.setFromNormalAndCoplanarPoint(tempVector, pivotPoint)
|
|
220
|
+
|
|
221
|
+
tempVector.copy(camera.position).sub(initialGrabPoint).normalize()
|
|
222
|
+
|
|
223
|
+
// if looking into the plane of rotation
|
|
224
|
+
if (Math.abs(tempVector.dot(plane.normal)) > 0.3) {
|
|
225
|
+
return super.getRevoluteDelta(joint, startPoint, endPoint)
|
|
226
|
+
} else {
|
|
227
|
+
// get the up direction
|
|
228
|
+
tempVector.set(0, 1, 0).transformDirection(camera.matrixWorld)
|
|
229
|
+
|
|
230
|
+
// get points projected onto the plane of rotation
|
|
231
|
+
plane.projectPoint(startPoint, projectedStartPoint)
|
|
232
|
+
plane.projectPoint(endPoint, projectedEndPoint)
|
|
233
|
+
|
|
234
|
+
tempVector.set(0, 0, -1).transformDirection(camera.matrixWorld)
|
|
235
|
+
tempVector.cross(plane.normal)
|
|
236
|
+
tempVector2.subVectors(endPoint, startPoint)
|
|
237
|
+
|
|
238
|
+
return tempVector.dot(tempVector2)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
dispose() {
|
|
243
|
+
const { domElement } = this
|
|
244
|
+
domElement.removeEventListener('pointerdown', this._mouseDown)
|
|
245
|
+
domElement.removeEventListener('pointermove', this._mouseMove)
|
|
246
|
+
domElement.removeEventListener('pointerup', this._mouseUp)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { customElement } from 'lit/decorators.js'
|
|
2
|
+
import * as THREE from 'three'
|
|
3
|
+
import { PointerURDFDragControls } from './urdf-drag-controls'
|
|
4
|
+
import { Material, MeshPhongMaterial } from 'three'
|
|
5
|
+
import URDFViewerElement from './urdf-viewer-element'
|
|
6
|
+
import { URDFJoint } from 'urdf-loader'
|
|
7
|
+
|
|
8
|
+
// urdf-manipulator element
|
|
9
|
+
// Displays a URDF model that can be manipulated with the mouse
|
|
10
|
+
|
|
11
|
+
// Events
|
|
12
|
+
// joint-mouseover: Fired when a joint is hovered over
|
|
13
|
+
// joint-mouseout: Fired when a joint is no longer hovered over
|
|
14
|
+
// manipulate-start: Fires when a joint is manipulated
|
|
15
|
+
// manipulate-end: Fires when a joint is done being manipulated
|
|
16
|
+
|
|
17
|
+
@customElement('urdf-viewer')
|
|
18
|
+
export default class URDFManipulatorElement extends URDFViewerElement {
|
|
19
|
+
static get observedAttributes() {
|
|
20
|
+
return ['highlight-color', ...super.observedAttributes]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
highlightMaterial: MeshPhongMaterial
|
|
24
|
+
dragControls: PointerURDFDragControls
|
|
25
|
+
|
|
26
|
+
get disableDragging() {
|
|
27
|
+
return this.hasAttribute('disable-dragging')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
set disableDragging(val) {
|
|
31
|
+
val ? this.setAttribute('disable-dragging', String(!!val)) : this.removeAttribute('disable-dragging')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get highlightColor() {
|
|
35
|
+
return this.getAttribute('highlight-color') || '#FFFFFF'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
set highlightColor(val) {
|
|
39
|
+
val ? this.setAttribute('highlight-color', val) : this.removeAttribute('highlight-color')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
constructor() {
|
|
43
|
+
super()
|
|
44
|
+
|
|
45
|
+
// The highlight material
|
|
46
|
+
this.highlightMaterial = new THREE.MeshPhongMaterial({
|
|
47
|
+
shininess: 10,
|
|
48
|
+
color: this.highlightColor,
|
|
49
|
+
emissive: this.highlightColor,
|
|
50
|
+
emissiveIntensity: 0.25
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const isJoint = (j: URDFJoint) => {
|
|
54
|
+
return j.isURDFJoint && j.jointType !== 'fixed'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Highlight the link geometry under a joint
|
|
58
|
+
const highlightLinkGeometry = (m: THREE.Material, revert: boolean) => {
|
|
59
|
+
const traverse = (c: any) => {
|
|
60
|
+
// Set or revert the highlight color
|
|
61
|
+
if (c.type === 'Mesh') {
|
|
62
|
+
if (revert) {
|
|
63
|
+
c.material = c.__origMaterial
|
|
64
|
+
delete c.__origMaterial
|
|
65
|
+
} else {
|
|
66
|
+
c.__origMaterial = c.material
|
|
67
|
+
c.material = this.highlightMaterial
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Look into the children and stop if the next child is
|
|
72
|
+
// another joint
|
|
73
|
+
if (c === m || !isJoint(c)) {
|
|
74
|
+
for (let i = 0; i < c.children.length; i++) {
|
|
75
|
+
traverse(c.children[i])
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
traverse(m)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const el = this.renderer.domElement
|
|
84
|
+
|
|
85
|
+
const dragControls = new PointerURDFDragControls(this.scene, this.camera, el)
|
|
86
|
+
|
|
87
|
+
dragControls.onDragStart = joint => {
|
|
88
|
+
this.dispatchEvent(new CustomEvent('manipulate-start', { bubbles: true, cancelable: true, detail: joint.name }))
|
|
89
|
+
this.controls.enabled = false
|
|
90
|
+
this.redraw()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
dragControls.onDragEnd = joint => {
|
|
94
|
+
this.dispatchEvent(new CustomEvent('manipulate-end', { bubbles: true, cancelable: true, detail: joint.name }))
|
|
95
|
+
this.controls.enabled = true
|
|
96
|
+
this.redraw()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
dragControls.updateJoint = (joint, angle) => {
|
|
100
|
+
this.setJointValue(joint.name, angle)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
dragControls.onHover = (joint: URDFJoint) => {
|
|
104
|
+
highlightLinkGeometry(joint as any, false)
|
|
105
|
+
this.dispatchEvent(new CustomEvent('joint-mouseover', { bubbles: true, cancelable: true, detail: joint.name }))
|
|
106
|
+
this.redraw()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
dragControls.onUnhover = joint => {
|
|
110
|
+
highlightLinkGeometry(joint as any, true)
|
|
111
|
+
this.dispatchEvent(new CustomEvent('joint-mouseout', { bubbles: true, cancelable: true, detail: joint.name }))
|
|
112
|
+
this.redraw()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.dragControls = dragControls
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
disconnectedCallback() {
|
|
119
|
+
super.disconnectedCallback()
|
|
120
|
+
this.dragControls.dispose()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
attributeChangedCallback(attr: string, oldval: any, newval: any) {
|
|
124
|
+
super.attributeChangedCallback(attr, oldval, newval)
|
|
125
|
+
|
|
126
|
+
switch (attr) {
|
|
127
|
+
case 'highlight-color':
|
|
128
|
+
this.highlightMaterial.color.set(this.highlightColor)
|
|
129
|
+
this.highlightMaterial.emissive.set(this.highlightColor)
|
|
130
|
+
break
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|