Skip to content

Commit 5f25044

Browse files
committed
Logitech MX Ink integration
1 parent 8ba2997 commit 5f25044

File tree

8 files changed

+675
-0
lines changed

8 files changed

+675
-0
lines changed

‎examples/index.html‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ <h2>Examples</h2>
144144
<li><a href="showcase/ui/">User Interface</a></li>
145145
<li><a href="showcase/link-traversal/">Link Traversal</a></li>
146146
<li><a href="showcase/model-viewer/">Model Viewer</a></li>
147+
<li><a href="showcase/painter/">Painter (Logitech MX Ink)</a></li>
147148
<li><a href="showcase/spatial-ui/">Spatial UI</a></li>
148149
<li><a href="showcase/tracked-controls/">Tracked Controls</a></li>
149150
<li><a href="showcase/shopping/">Shopping</a></li>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/* globals AFRAME THREE */
2+
AFRAME.registerComponent('brush', {
3+
schema: {
4+
color: {type: 'color', default: '#ef2d5e'},
5+
size: {default: 0.01, min: 0.001, max: 0.3},
6+
enabled: {default: true},
7+
hand: {default: 'left'}
8+
},
9+
10+
init: function () {
11+
var data = this.data;
12+
var el = this.el;
13+
this.color = new THREE.Color(data.color);
14+
this.painting = false;
15+
this.stroke = null;
16+
this.buttonsDown = 0;
17+
18+
this.onButtonDown = this.onButtonDown.bind(this);
19+
el.addEventListener('buttondown', this.onButtonDown);
20+
21+
this.onButtonUp = this.onButtonUp.bind(this);
22+
el.addEventListener('buttonup', this.onButtonUp);
23+
24+
this.onControllerConnected = this.onControllerConnected.bind(this);
25+
el.addEventListener('controllerconnected', this.onControllerConnected);
26+
27+
this.el.setAttribute('oculus-touch-controls', {hand: this.data.hand});
28+
this.el.setAttribute('logitech-mx-ink-controls', {hand: this.data.hand});
29+
},
30+
31+
onControllerConnected: function (evt) {
32+
this.hand = evt.target.getAttribute(evt.detail.name).hand;
33+
this.controllerName = evt.detail.name;
34+
},
35+
36+
onButtonDown: function () {
37+
if (!this.data.enabled) { return; }
38+
this.buttonsDown++;
39+
this.startNewStroke();
40+
this.painting = true;
41+
},
42+
43+
onButtonUp: function () {
44+
if (!this.data.enabled) { return; }
45+
this.buttonsDown--;
46+
if (this.buttonDown > 0) { return; }
47+
if (!this.painting) { return; }
48+
this.stroke = null;
49+
this.painting = false;
50+
},
51+
52+
tick: (function () {
53+
var position = new THREE.Vector3();
54+
var rotation = new THREE.Quaternion();
55+
var scale = new THREE.Vector3();
56+
57+
return function tick (time, delta) {
58+
if (!this.painting || !this.stroke) { return; }
59+
this.el.object3D.matrixWorld.decompose(position, rotation, scale);
60+
var pointerPosition = this.getPointerPosition(position, rotation);
61+
this.stroke.addPoint(position, rotation, pointerPosition);
62+
};
63+
})(),
64+
65+
startNewStroke: function () {
66+
this.stroke = this.system.addNewStroke(this.color, this.data.size);
67+
},
68+
69+
getPointerPosition: (function () {
70+
var pointerPosition = new THREE.Vector3();
71+
var pointerOffset = new THREE.Vector3();
72+
var controllerOffset = {
73+
'oculus-touch-controls': {
74+
left: new THREE.Vector3(0, -0.025, -0.04),
75+
right: new THREE.Vector3(0, -0.025, -0.04)
76+
},
77+
'logitech-mx-ink-controls': {
78+
left: new THREE.Vector3(0, -0.065, -0.07),
79+
right: new THREE.Vector3(0, -0.065, -0.07)
80+
}
81+
};
82+
83+
return function getPointerPosition (position, orientation) {
84+
if (!this.controllerName) { return position; }
85+
var offsets = controllerOffset[this.controllerName];
86+
pointerOffset.copy(offsets[this.hand]);
87+
pointerOffset.applyQuaternion(orientation);
88+
pointerPosition.copy(position).add(pointerOffset);
89+
return pointerPosition;
90+
};
91+
})()
92+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Painter (Logitech MX INK) • A-Frame</title>
6+
<meta name="description" content="Hand Tracking! • A-Frame">
7+
<script src="../../../dist/aframe-master.js"></script>
8+
<script src="https://unpkg.com/aframe-environment-component@1.3.x/dist/aframe-environment-component.min.js"></script>
9+
<script src="stroke-geometry.js"></script>
10+
<script src="components/brush.js"></script>
11+
<script src="systems/brush.js"></script>
12+
<script src="../../js/info-message.js"></script>
13+
</head>
14+
<body>
15+
<a-scene
16+
xr-mode-ui="XRMode: xr"
17+
info-message="htmlSrc: #messageText">
18+
<a-assets>
19+
<a-asset-item id="messageText" src="message.html"></a-asset-item>
20+
</a-assets>
21+
<a-entity environment hide-on-enter-ar></a-entity>
22+
<a-entity
23+
id="right-hand"
24+
brush="hand: right">
25+
</a-entity>
26+
<a-entity
27+
id="left-hand"
28+
brush="hand: left">
29+
</a-entity>
30+
</a-scene>
31+
</body>
32+
</html>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<p>Example that illustrates integration of the Logitech MX Ink tracked pen.
2+
Press any button to draw.
3+
</p>
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
/* globals THREE */
2+
function StrokeGeometry (material, entityEl) {
3+
this.material = material;
4+
this.maxBufferSize = 5000;
5+
this.geometries = [];
6+
this.currentGeometry = null;
7+
this.entityEl = entityEl;
8+
this.addBuffer(false);
9+
this.first = true;
10+
}
11+
12+
StrokeGeometry.prototype = {
13+
addBuffer: function (copyLast) {
14+
var geometry = new THREE.BufferGeometry();
15+
16+
var vertices = new Float32Array(this.maxBufferSize * 3);
17+
var indices = new Uint32Array(this.maxBufferSize * 4.5);
18+
var normals = new Float32Array(this.maxBufferSize * 3);
19+
var uvs = new Float32Array(this.maxBufferSize * 2);
20+
var colors = new Float32Array(this.maxBufferSize * 3);
21+
22+
var mesh = new THREE.Mesh(geometry, this.material);
23+
24+
mesh.frustumCulled = false;
25+
mesh.vertices = vertices;
26+
27+
this.object3D = new THREE.Object3D();
28+
this.object3D.add(mesh);
29+
30+
this.entityEl.object3D.add(this.object3D);
31+
32+
geometry.setDrawRange(0, 0);
33+
geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3).setUsage(THREE.DynamicDrawUsage));
34+
geometry.attributes.position.updateRanges.count = 0;
35+
36+
geometry.setIndex(new THREE.BufferAttribute(indices, 3).setUsage(THREE.DynamicDrawUsage));
37+
geometry.index.updateRanges.count = 0;
38+
39+
geometry.setAttribute('uv', new THREE.BufferAttribute(uvs, 2).setUsage(THREE.DynamicDrawUsage));
40+
geometry.attributes.uv.updateRanges.count = 0;
41+
42+
geometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3).setUsage(THREE.DynamicDrawUsage));
43+
geometry.attributes.normal.updateRanges.count = 0;
44+
45+
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3).setUsage(THREE.DynamicDrawUsage));
46+
geometry.attributes.color.updateRanges.count = 0;
47+
48+
this.previousGeometry = null;
49+
if (this.geometries.length > 0) {
50+
this.previousGeometry = this.currentGeometry;
51+
}
52+
53+
this.indices = {
54+
position: 0,
55+
index: 0,
56+
uv: 0,
57+
normal: 0,
58+
color: 0
59+
};
60+
61+
this.prevIndices = Object.assign({}, this.indices);
62+
63+
this.geometries.push(geometry);
64+
this.currentGeometry = geometry;
65+
66+
// Copies first position from previous buffer so they are continous with no gaps.
67+
// Necessary for example for drawing strokes.
68+
if (this.previousGeometry && copyLast) {
69+
var prev = (this.maxBufferSize - 2) * 3;
70+
var col = (this.maxBufferSize - 2) * 3;
71+
var uv = (this.maxBufferSize - 2) * 2;
72+
var norm = (this.maxBufferSize - 2) * 3;
73+
74+
var position = this.previousGeometry.attributes.position.array;
75+
this.addVertex(position[prev++], position[prev++], position[prev++]);
76+
this.addVertex(position[prev++], position[prev++], position[prev++]);
77+
78+
var normal = this.previousGeometry.attributes.normal.array;
79+
this.addNormal(normal[norm++], normal[norm++], normal[norm++]);
80+
this.addNormal(normal[norm++], normal[norm++], normal[norm++]);
81+
82+
var color = this.previousGeometry.attributes.color.array;
83+
this.addColor(color[col++], color[col++], color[col++]);
84+
this.addColor(color[col++], color[col++], color[col++]);
85+
86+
uvs = this.previousGeometry.attributes.uv.array;
87+
}
88+
},
89+
90+
addColor: function (r, g, b) {
91+
this.currentGeometry.attributes.color.setXYZ(this.indices.color++, r, g, b);
92+
},
93+
94+
addNormal: function (x, y, z) {
95+
this.currentGeometry.attributes.normal.setXYZ(this.indices.normal++, x, y, z);
96+
},
97+
98+
addPoint: (function () {
99+
var direction = new THREE.Vector3();
100+
var vertexA = new THREE.Vector3();
101+
var vertexAOffset = new THREE.Vector3();
102+
103+
var vertexB = new THREE.Vector3();
104+
var vertexBOffset = new THREE.Vector3();
105+
106+
return function (position, orientation, width) {
107+
direction.set(1, 0, 0);
108+
direction.applyQuaternion(orientation);
109+
direction.normalize();
110+
111+
// Add two vertices to the triangle strip separated by the brush size.
112+
vertexA.copy(position);
113+
vertexB.copy(position);
114+
115+
vertexA.add(vertexAOffset.copy(direction).multiplyScalar(width / 2));
116+
vertexB.add(vertexAOffset.copy(direction).multiplyScalar(-width / 2));
117+
118+
// if (this.first && this.indices.position > 0) {
119+
// debugger;
120+
// // Degenerated triangle
121+
// this.first = false;
122+
// this.addVertex(vertexA.x, vertexA.y, vertexA.z);
123+
// this.indices.normal++;
124+
// this.indices.color++;
125+
// this.indices.uv++;
126+
// }
127+
128+
/*
129+
2---3
130+
| \ |
131+
0---1
132+
*/
133+
this.addVertex(vertexA.x, vertexA.y, vertexA.z);
134+
this.addVertex(vertexB.x, vertexB.y, vertexB.z);
135+
this.indices.normal += 2;
136+
137+
this.addColor(this.material.color.r, this.material.color.g, this.material.color.b);
138+
this.addColor(this.material.color.r, this.material.color.g, this.material.color.b);
139+
140+
this.update();
141+
this.computeVertexNormals();
142+
};
143+
})(),
144+
145+
computeVertexNormals: (function () {
146+
var pA = new THREE.Vector3();
147+
var pB = new THREE.Vector3();
148+
var pC = new THREE.Vector3();
149+
var cb = new THREE.Vector3();
150+
var ab = new THREE.Vector3();
151+
152+
return function () {
153+
var start = this.prevIndices.position === 0 ? 0 : (this.prevIndices.position + 1) * 3;
154+
var end = (this.indices.position) * 3;
155+
var vertices = this.currentGeometry.attributes.position.array;
156+
var normals = this.currentGeometry.attributes.normal.array;
157+
158+
for (var i = start; i <= end; i++) {
159+
normals[i] = 0;
160+
}
161+
162+
var pair = true;
163+
for (i = start; i < end - 6; i += 3) {
164+
if (pair) {
165+
pA.fromArray(vertices, i);
166+
pB.fromArray(vertices, i + 3);
167+
pC.fromArray(vertices, i + 6);
168+
} else {
169+
pB.fromArray(vertices, i);
170+
pC.fromArray(vertices, i + 6);
171+
pA.fromArray(vertices, i + 3);
172+
}
173+
pair = !pair;
174+
175+
cb.subVectors(pC, pB);
176+
ab.subVectors(pA, pB);
177+
cb.cross(ab);
178+
cb.normalize();
179+
180+
normals[i] += cb.x;
181+
normals[i + 1] += cb.y;
182+
normals[i + 2] += cb.z;
183+
184+
normals[i + 3] += cb.x;
185+
normals[i + 4] += cb.y;
186+
normals[i + 5] += cb.z;
187+
188+
normals[i + 6] += cb.x;
189+
normals[i + 7] += cb.y;
190+
normals[i + 8] += cb.z;
191+
}
192+
193+
/*
194+
first and last vertices (0 and 8) belongs just to one triangle
195+
second and penultimate (1 and 7) belongs to two triangles
196+
the rest of the vertices belongs to three triangles
197+
198+
1_____3_____5_____7
199+
/\ /\ /\ /\
200+
/ \ / \ / \ / \
201+
/____\/____\/____\/____\
202+
0 2 4 6 8
203+
*/
204+
205+
// Vertices that are shared across three triangles
206+
for (i = start + 2 * 3; i < end - 2 * 3; i++) {
207+
normals[i] = normals[i] / 3;
208+
}
209+
210+
// Second and penultimate triangle, that shares just two triangles
211+
normals[start + 3] = normals[start + 3] / 2;
212+
normals[start + 3 + 1] = normals[start + 3 + 1] / 2;
213+
normals[start + 3 + 2] = normals[start + 3 * 1 + 2] / 2;
214+
215+
normals[end - 2 * 3] = normals[end - 2 * 3] / 2;
216+
normals[end - 2 * 3 + 1] = normals[end - 2 * 3 + 1] / 2;
217+
normals[end - 2 * 3 + 2] = normals[end - 2 * 3 + 2] / 2;
218+
};
219+
})(),
220+
221+
addVertex: function (x, y, z) {
222+
var buffer = this.currentGeometry.attributes.position;
223+
// Initialize a new buffer if full;
224+
if (this.indices.position === buffer.count) {
225+
this.addBuffer(true);
226+
buffer = this.currentGeometry.attributes.position;
227+
}
228+
buffer.setXYZ(this.indices.position++, x, y, z);
229+
// Every two new vertices we add two new triangles.
230+
if ((this.indices.position + 1) % 2 === 0 && this.indices.position > 1) {
231+
/* Line brushes
232+
2---3
233+
| \ |
234+
0---1
235+
{0, 1, 2}, {2, 1, 3}
236+
*/
237+
this.currentGeometry.index.setXYZ(this.indices.index++, this.indices.position - 3, this.indices.position - 2, this.indices.position - 1);
238+
this.currentGeometry.index.setXYZ(this.indices.index++, this.indices.position - 1, this.indices.position - 2, this.indices.position);
239+
}
240+
},
241+
242+
update: function () {
243+
// Draw one less triangle to prevent indexing into blank positions
244+
// on an even-number-positioned undo
245+
this.currentGeometry.setDrawRange(0, (this.indices.position * 3) - 4);
246+
247+
this.currentGeometry.attributes.color.updateRanges.count = this.indices.position * 3;
248+
this.currentGeometry.attributes.color.needsUpdate = true;
249+
this.currentGeometry.attributes.normal.updateRanges.count = this.indices.position * 3;
250+
this.currentGeometry.attributes.normal.needsUpdate = true;
251+
this.currentGeometry.attributes.position.updateRanges.count = this.indices.position * 3;
252+
this.currentGeometry.attributes.position.needsUpdate = true;
253+
this.currentGeometry.attributes.uv.updateRanges.count = this.indices.position * 2;
254+
this.currentGeometry.attributes.uv.needsUpdate = true;
255+
this.currentGeometry.index.updateRanges.count = this.indices.position * 3;
256+
this.currentGeometry.index.needsUpdate = true;
257+
}
258+
};

0 commit comments

Comments
 (0)