diff --git a/README.md b/README.md
index 8a3804a..4bc457f 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ You can create a shape from a set of points and build either a:
- area
- volume
-
+
## Installation
@@ -28,10 +28,24 @@ const shape = new Shape3D();
scene.add(shape);
```
-## API
+### Line
TODO
+### Area
+
+TODO
+
+### Volume
+
+TODO
+
+### Labels
+
+TODO
+
+
+
## Example
See the [example](./example) folder for an example.
@@ -53,23 +67,23 @@ This is a work in progress. It's not ready for production use.
The design is based on the following:
-![Design](https://github.com/andrewisen-tikab/three-shape-3d/blob/feature/resources/definition-01.png 'Design')
+![Design](https://github.com/andrewisen-tikab/three-shape-3d/blob/feature/resources/definition-01.png?raw=true 'Design')
A line is defined by a set of 3-dimensional vertices. The point between two vertices is called `midpoint`.
-![Design](https://github.com/andrewisen-tikab/three-shape-3d/blob/feature/resources/definition-02.png 'Design')
+![Design](https://github.com/andrewisen-tikab/three-shape-3d/blob/feature/resources/definition-02.png?raw=true 'Design')
The first vertex is called the `startpoint` and the last is called the `endpoint`.
-![Design](https://github.com/andrewisen-tikab/three-shape-3d/blob/feature/resources/definition-03.png 'Design')
+![Design](https://github.com/andrewisen-tikab/three-shape-3d/blob/feature/resources/definition-03.png?raw=true 'Design')
A line can be closed by setting `closeLine` to `true`.
-![Design](https://github.com/andrewisen-tikab/three-shape-3d/blob/feature/resources/definition-04.png 'Design')
+![Design](https://github.com/andrewisen-tikab/three-shape-3d/blob/feature/resources/definition-04.png?raw=true 'Design')
An area is a line with three or more vertices.
-![Design](https://github.com/andrewisen-tikab/three-shape-3d/blob/feature/resources/definition-05.png 'Design')
+![Design](https://github.com/andrewisen-tikab/three-shape-3d/blob/feature/resources/definition-05.png?raw=true 'Design')
A volume is an area with a positive height.
diff --git a/resources/labels.gif b/resources/labels.gif
new file mode 100644
index 0000000..d0907f6
Binary files /dev/null and b/resources/labels.gif differ
diff --git a/src/controls/TransformShapeControls/LabelsManager.ts b/src/controls/TransformShapeControls/LabelsManager.ts
new file mode 100644
index 0000000..6486855
--- /dev/null
+++ b/src/controls/TransformShapeControls/LabelsManager.ts
@@ -0,0 +1,158 @@
+import * as THREE from 'three';
+import type { TransformShapeControls } from './TransformShapeControls';
+import { Vertex } from '../../types';
+import { getLength2D, getMidpointOffsetFromLine } from '../../utils';
+import { addPrefix } from './labels';
+import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
+
+/**
+ * Manages the labels for the {@link TransformShapeControls}.
+ */
+export default class LabelsManager extends THREE.EventDispatcher {
+ private transformShapeControls: TransformShapeControls;
+
+ private labels: CSS2DObject[] = [];
+
+ constructor(transformShapeControls: TransformShapeControls) {
+ super();
+ this.transformShapeControls = transformShapeControls;
+ this.labels = [];
+ }
+
+ /**
+ * Call this every frame.
+ */
+ public update() {
+ this.updatePositions();
+ }
+
+ /**
+ * Add new labels to the object.
+ */
+ public addLabels() {
+ if (!this.transformShapeControls.object) return;
+ this.labels = [];
+ const vertices = this.transformShapeControls.object.getVertices();
+
+ // Get distance from camera to center of object
+ const offsetDistance = 1;
+
+ const center: Vertex = [
+ this.transformShapeControls.vertexCenter.x,
+ this.transformShapeControls.vertexCenter.y,
+ this.transformShapeControls.vertexCenter.z,
+ ];
+ for (let index = 0; index < vertices.length; index++) {
+ const vertex = vertices[index];
+ if (index === 0) continue;
+ const previousVertex = vertices[index - 1];
+ const label = this.generateLabel(
+ this.transformShapeControls.labelsGroup,
+ index,
+ vertex,
+ previousVertex,
+ center,
+ offsetDistance,
+ );
+ this.labels.push(label);
+ }
+
+ if (this.transformShapeControls.object!.getCloseLine()) {
+ const vertex = vertices[0];
+ const previousVertex = vertices[vertices.length - 1];
+
+ const label = this.generateLabel(
+ this.transformShapeControls.labelsGroup,
+ vertices.length,
+ vertex,
+ previousVertex,
+ center,
+ offsetDistance,
+ );
+ this.labels.push(label);
+ }
+ }
+
+ /**
+ * Updates the position of the labels based on the camera's distance.
+ */
+ private updatePositions(): void {
+ if (!this.transformShapeControls.object) return;
+
+ const vertices = this.transformShapeControls.object.getVertices();
+ const offsetDistance =
+ this.transformShapeControls.vertexCenter.distanceTo(
+ this.transformShapeControls.camera.position,
+ ) /
+ 100 +
+ 1.1;
+
+ const center: Vertex = [
+ this.transformShapeControls.vertexCenter.x,
+ this.transformShapeControls.vertexCenter.y,
+ this.transformShapeControls.vertexCenter.z,
+ ];
+ for (let i = 0; i < this.labels.length; i++) {
+ const label = this.labels[i];
+ const firstVertex = vertices[i];
+ const secondVertex = i === vertices.length - 1 ? vertices[0] : vertices[i + 1];
+ this.updatePosition(label, firstVertex, secondVertex, center, offsetDistance);
+ }
+ }
+
+ private updatePosition(
+ label: CSS2DObject,
+ firstVertex: Vertex,
+ secondVertex: Vertex,
+ center: Vertex,
+ offsetDistance: number,
+ ) {
+ const offset = getMidpointOffsetFromLine(firstVertex, secondVertex, center, offsetDistance);
+ label.position.set(offset[0], offset[1], offset[2]);
+ }
+
+ private generateLabel(
+ parent: THREE.Object3D,
+ index: number,
+ firstVertex: Vertex,
+ secondVertex: Vertex,
+ center: Vertex,
+ offsetDistance: number,
+ ): CSS2DObject {
+ const length = getLength2D(firstVertex, secondVertex);
+
+ const divElement = document.createElement('div');
+ divElement.className = 'shape-3d-label-container';
+
+ const inputElement = document.createElement('input');
+ inputElement.type = 'text';
+
+ inputElement.id = 'myInput';
+ inputElement.className = 'shape-3d-label';
+ inputElement.placeholder = `${length.toFixed(2)}`;
+ inputElement.setAttribute('size', `${inputElement.getAttribute('placeholder')!.length}`);
+ inputElement.oninput = addPrefix.bind(inputElement);
+
+ inputElement.addEventListener('blur', (e) => {
+ this.onBlur(e, index);
+ });
+
+ divElement.appendChild(inputElement);
+ const label = new CSS2DObject(divElement);
+ this.updatePosition(label, firstVertex, secondVertex, center, offsetDistance);
+ parent.add(label);
+
+ return label;
+ }
+
+ onBlur(e: FocusEvent, index: number) {
+ // Get value of input
+ const inputElement = e.target as HTMLInputElement;
+ const value = inputElement.value;
+
+ const number = parseFloat(value);
+ if (isNaN(number)) return;
+ if (number <= 0) return;
+ this.transformShapeControls.setLineLength(index, number);
+ }
+}
diff --git a/src/controls/TransformShapeControls/TransformShapeControls.ts b/src/controls/TransformShapeControls/TransformShapeControls.ts
index 0ad0602..71a5e38 100644
--- a/src/controls/TransformShapeControls/TransformShapeControls.ts
+++ b/src/controls/TransformShapeControls/TransformShapeControls.ts
@@ -8,9 +8,8 @@ THREE.BufferGeometry.prototype.disposeBoundsTree = disposeBoundsTree;
THREE.Mesh.prototype.raycast = acceleratedRaycast;
import Shape3D from '../../Shape3D';
-import { getMidpoint } from '../../utils';
-import { generateLabel } from './labels';
-import { Vertex } from '../../types';
+import { getMidpoint, setLineLength } from '../../utils';
+import LabelsManager from './LabelsManager';
const _raycaster = new THREE.Raycaster();
// @ts-ignore
@@ -50,9 +49,11 @@ type VertexMetadata = {
type Mode = 'translate' | 'rotate' | 'scale';
class TransformShapeControls extends THREE.Object3D {
- private vertexGroup!: THREE.Group;
+ public vertexGroup!: THREE.Group;
- private labelsGroup!: THREE.Group;
+ public labelsGroup!: THREE.Group;
+
+ private labelsManager: LabelsManager;
public object?: Shape3D;
@@ -149,7 +150,7 @@ class TransformShapeControls extends THREE.Object3D {
private _onPointerUp!: (event: any) => void;
params: Partial;
- private vertexCenter: THREE.Vector3;
+ public vertexCenter: THREE.Vector3;
private lastSelectedVertex: THREE.Mesh | null = null;
private lastSelectedVertexQuaternion: THREE.Quaternion;
@@ -263,6 +264,7 @@ class TransformShapeControls extends THREE.Object3D {
this.vertexHoverMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
this.add(vertexGroup);
+ this.labelsManager = new LabelsManager(this);
const labelsGroup = new THREE.Group();
this.add(labelsGroup);
@@ -429,6 +431,8 @@ class TransformShapeControls extends THREE.Object3D {
this.eye.copy(this.cameraPosition).sub(this.worldPosition).normalize();
}
+ this.labelsManager.update();
+
// @ts-ignore
super.updateMatrixWorld(this);
}
@@ -949,26 +953,7 @@ class TransformShapeControls extends THREE.Object3D {
private addLabels() {
this.labelsGroup.clear();
- const vertices = this.object!.getVertices();
-
- // Get distance from camera to center of object
- const offsetDistance = this.vertexCenter.distanceTo(this.camera.position) / 100 + 1.1;
-
- const center: Vertex = [this.vertexCenter.x, this.vertexCenter.y, this.vertexCenter.z];
- for (let index = 0; index < vertices.length; index++) {
- const vertex = vertices[index];
- if (index === 0) continue;
- const previousVertex = vertices[index - 1];
- generateLabel(this.labelsGroup, vertex, previousVertex, center, offsetDistance);
- }
-
- if (this.object!.getCloseLine()) {
- const vertex = vertices[0];
- const previousVertex = vertices[vertices.length - 1];
-
- generateLabel(this.labelsGroup, vertex, previousVertex, center, offsetDistance);
- }
-
+ this.labelsManager.addLabels();
this.labelsGroup.position.set(-this.position.x, -this.position.y, -this.position.z);
}
@@ -1040,6 +1025,26 @@ class TransformShapeControls extends THREE.Object3D {
setSpace(space: string) {
this.space = space;
}
+
+ setLineLength(index: number, lineLength: number) {
+ if (this.object == null) {
+ console.warn('No object attached');
+ return;
+ }
+ if (index < 0 || index > this.object!.getVertices().length) {
+ console.warn('Invalid index');
+ return;
+ }
+ if (lineLength < 0) {
+ console.warn('Invalid line length');
+ return;
+ }
+ const adjustedIndex = index === this.object!.getVertices().length ? 0 : index;
+ const vertices = this.object!.getVertices();
+ const newVertex = setLineLength(index, lineLength, vertices);
+ this.object.updateVertex(adjustedIndex, newVertex);
+ this.onVertexChanged();
+ }
}
// mouse / touch event handlers
diff --git a/src/controls/TransformShapeControls/labels.ts b/src/controls/TransformShapeControls/labels.ts
index 2ee0415..9a57352 100644
--- a/src/controls/TransformShapeControls/labels.ts
+++ b/src/controls/TransformShapeControls/labels.ts
@@ -2,10 +2,18 @@ import { CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
import { Vertex } from '../../types';
import { getLength2D, getMidpointOffsetFromLine } from '../../utils';
-function addPrefix(this: HTMLInputElement, _ev: Event) {
+export function addPrefix(this: HTMLInputElement, _ev: Event) {
this.setAttribute('size', `${this.value!.length}`);
}
+/**
+ * @deprecated Use {@link LabelsManager} instead.
+ * @param parent
+ * @param firstVertex
+ * @param secondVertex
+ * @param center
+ * @param offsetDistance
+ */
export const generateLabel = (
parent: THREE.Object3D,
firstVertex: Vertex,
diff --git a/src/utils.ts b/src/utils.ts
index 15dfb5d..6e63eb3 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -76,3 +76,17 @@ export const getLength2D = (firstVertex: Vertex, secondVertex: Vertex): number =
Math.pow(firstVertex[2] - secondVertex[2], 2),
);
};
+
+export const setLineLength = (index: number, lineLength: number, vertices: Vertex[]): Vertex => {
+ const closedLine = vertices.length === index;
+
+ const firstVertex = closedLine ? vertices[vertices.length - 1] : vertices[index - 1];
+ const secondVertex = closedLine ? vertices[0] : vertices[index];
+
+ _firstVertex.fromArray(firstVertex);
+ _secondVertex.fromArray(secondVertex);
+
+ _line.subVectors(_secondVertex, _firstVertex).normalize();
+ _firstVertex.add(_line.multiplyScalar(lineLength));
+ return _firstVertex.toArray();
+};