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(); +};