-
Notifications
You must be signed in to change notification settings - Fork 0
/
BusinessLogic.js
218 lines (199 loc) · 8.84 KB
/
BusinessLogic.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
/**
* Controller for business logic
* Using Dependency Injection to use Models Service props and methods
*/
class BusinessLogic {
// Injects ThreeJS Models with Dependency Injection
constructor(modelsInstance) {
this.state = {
isSameFace: false,
previousFaceIndex: undefined,
// temporarily stores indices of selected face's vertices, for a given control point
selectedFaces: [], // format: [ [vertexAIndex, vertexBIndex, vertexCIndex], ... ]
};
this.modelsInstance = modelsInstance;
// DOM events listeners
const canvaGL = this.modelsInstance.container.getElementsByTagName('canvas')[0];
canvaGL.addEventListener('mousemove', this.onMouseMove, false);
['mousedown', 'mouseup'].forEach((type) => canvaGL.addEventListener(type, this.onClick, false));
}
// Init selected faces at mount phase (but after model is rendered, hence the .then in app.js)
initSelectedFaces = () => {
const Model = this.modelsInstance.scene.getObjectByName('Model');
this.modelColors = Model.geometry.getAttribute('color');
const selectionColor = this.modelsInstance.configColors.selection.toArray();
console.log({
'Persisted faces': controlPoints.reduce((acc, cp) => acc.concat(cp.faces), []),
});
for (let cp of controlPoints) {
for (let faceVertices of cp.faces) {
this.setFaceColor(faceVertices, selectionColor);
}
}
};
initModelAndCameraEventListeners = () => {
const Model = this.modelsInstance.scene.getObjectByName('Model');
const Camera = this.modelsInstance.camera;
const modelPositionsButtons = this.modelsInstance.container.querySelectorAll(
'.model-positions button',
);
const povButtons = this.modelsInstance.container.querySelectorAll('.view-commands button');
const handlersMapping = {
reset: () => Camera.position.fromArray(this.modelsInstance.config.cameraPosition),
opposite: () => Camera.position.set(2, 2, -3),
face: () => this.modelsInstance.rotateModel(0, 0, 1),
behind: () => this.modelsInstance.rotateModel(0, 0, -1),
left: () => this.modelsInstance.rotateModel(1, 0, 0),
right: () => this.modelsInstance.rotateModel(-1, 0, 0),
above: () => this.modelsInstance.rotateModel(0, -1, 0),
below: () => this.modelsInstance.rotateModel(0, 1, 0),
};
modelPositionsButtons.forEach((button) =>
button.addEventListener('click', handlersMapping[button.name]),
);
povButtons.forEach((button) => button.addEventListener('click', handlersMapping[button.name]));
const fullscreenButton = document.getElementById('fullscreen');
fullscreenButton.addEventListener('click', () =>
document.fullscreenElement
? document.exitFullscreen()
: this.modelsInstance.container.requestFullscreen(),
);
};
onMouseMove = (event) => {
event.preventDefault();
const faceEdge = this.modelsInstance.scene.getObjectByName('Face Edge');
const targetPointer = this.modelsInstance.scene.getObjectByName('Target Pointer');
const intersectionWithModel = this.modelsInstance.getIntersection(event);
// Add condition '&& event.ctrlKey' to show pointer and edges only whith Ctrl key down
// TODO: add a config option for that ?
if (intersectionWithModel) {
this.state.isSameFace = intersectionWithModel.faceIndex === this.state.previousFaceIndex;
!this.state.isSameFace && (this.state.previousFaceIndex = intersectionWithModel.faceIndex);
!this.state.isSameFace && console.log({ intersectionWithModel });
this.showTargetPointer(
targetPointer,
intersectionWithModel.point,
intersectionWithModel.face.normal,
);
this.highLightEdges(faceEdge, intersectionWithModel.face, intersectionWithModel.object);
this.toggleSelections(event, intersectionWithModel.face, this.state.isSameFace);
} else {
this.state.previousFaceIndex = undefined;
faceEdge && (faceEdge.visible = false);
targetPointer && (targetPointer.visible = false);
}
};
onClick = (event) => {
event.preventDefault();
const intersectionWithModel = this.modelsInstance.getIntersection(event);
if (intersectionWithModel) {
// Prevents camera moving while mouse dragging
this.modelsInstance.controls.enabled = !event.ctrlKey || event.type === 'mouseup';
event.type === 'mousedown'
? this.toggleSelections(event, intersectionWithModel.face)
: this.validateSelections(); // @ mouseup
} else {
this.state.previousFaceIndex = undefined;
const faceEdge = this.modelsInstance.scene.getObjectByName('Face Edge');
faceEdge && (faceEdge.visible = false);
}
};
// Face selection triggers by mousemove event only if current hovered face changed
toggleSelections = (event, face, isSameFace = false) => {
if (event.ctrlKey && event.which === 1 && !isSameFace) {
const { base, selection } = this.modelsInstance.configColors;
const faceVertices = [face.a, face.b, face.c];
// const modelColors = intersection.object.geometry.getAttribute('color');
const foundStep = this.findStepWithFace(faceVertices);
if (!this.isFaceSelected(faceVertices)) {
// Selection
if (!foundStep) {
this.setFaceColor(faceVertices, selection.toArray());
this.state.selectedFaces.push(faceVertices); // FIXME: /!\ Take care of duplicates !
} else console.info(`Ignoring this face since it is already accounted in step.`);
} else {
// Deselection
this.setFaceColor(faceVertices, base.toArray());
if (!foundStep) {
this.deleteFromState(faceVertices);
} else {
if (foundStep.faces.length === 1) {
const index2remove = controlPoints.findIndex((cp) => cp.step === foundStep.step);
controlPoints.splice(index2remove, 1);
} else {
const index2remove = foundStep.faces.findIndex((f) =>
this.isSameFaceVertices(f, faceVertices),
);
foundStep.faces.splice(index2remove, 1);
}
}
}
}
};
validateSelections() {
this.state.selectedFaces.length && this.createControlPoint();
console.log('BusinessLogic createControlPoint', controlPoints);
this.modelsInstance.updateDOM();
}
deleteFromState(face) {
const index2remove = this.state.selectedFaces.findIndex((f) =>
this.isSameFaceVertices(f, face),
);
this.state.selectedFaces.splice(index2remove, 1);
}
// Saves selected faces, under a new step,
// 1 step = 1 control point, 1 control point can include multiple faces (at least 1)
// Beware to not store twice same face index (CPs should not overlap, at exception of vertices on CP's edges)
// if overlapping occurs, call editControlPoint() to merge faces
createControlPoint() {
controlPoints.push({
step: 'Step ' + controlPoints.length, // TODO: add a prompt so user can set a custom value
faces: this.state.selectedFaces,
});
this.state.selectedFaces = [];
}
// Checks if a step already has some faces in common with selected one
findStepWithFace(face) {
return controlPoints.find(({ faces }) => faces.some((f) => this.isSameFaceVertices(f, face)));
}
setFaceColor(faceVertices, color2apply) {
// can not use 'Face' with a BufferGeometry,
// so have to color the 3 vertices representing the "face"
// dispatches selectionColor.r/g/b to modelColors.x/y/z
faceVertices.forEach((v) => this.modelColors.setXYZ(v, ...color2apply));
this.modelColors.needsUpdate = true;
}
// colors edges around the hovered face
highLightEdges(edge, hoveredFace, Model) {
const edgePosition = edge.geometry.attributes.position;
const modelPosition = Model.geometry.attributes.position;
edgePosition.copyAt(0, modelPosition, hoveredFace.a);
edgePosition.copyAt(1, modelPosition, hoveredFace.b);
edgePosition.copyAt(2, modelPosition, hoveredFace.c);
edgePosition.copyAt(3, modelPosition, hoveredFace.a);
Model.updateMatrix();
edge.geometry.applyMatrix(Model.matrix);
edge.visible = true;
}
showTargetPointer(targetPointer, point, normal) {
targetPointer.position.set(0, 0, 0);
targetPointer.lookAt(normal);
point.add(normal.multiplyScalar(0.0005)); // adds some coords along the face's normal so the pointer appears slightly above the face
targetPointer.position.copy(point);
targetPointer.visible = true;
}
getVertexColor(vertexIndex) {
return new THREE.Color(
this.modelColors.getX(vertexIndex),
this.modelColors.getY(vertexIndex),
this.modelColors.getZ(vertexIndex),
);
}
isSameFaceVertices(face1, face2) {
return face1.every((v, i) => v === face2[i]);
}
isFaceSelected(faceVertices) {
const selectionColor = this.modelsInstance.configColors.selection.getHexString();
return faceVertices.every((v) => this.getVertexColor(v).getHexString() === selectionColor);
}
}