@vector-display/3d 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rick Segrest
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @vector-display/3d
2
+
3
+ 3D wireframe models and perspective projection for [**vector-display**](https://www.npmjs.com/package/@vector-display/core), for *Battlezone*-, *Tempest*- and *Star Wars*-style graphics. The math is built on [es-vector-math](https://www.npmjs.com/package/es-vector-math).
4
+
5
+ > **Status:** early (0.x). The API may change between minor versions.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install @vector-display/core @vector-display/3d es-vector-math
11
+ ```
12
+
13
+ ES modules only, with TypeScript declarations.
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { DisplayList, SCREEN_SPACE_PLACEMENT } from "@vector-display/core";
19
+ import {
20
+ DEFAULT_PERSPECTIVE_CAMERA,
21
+ WireframeProjector,
22
+ createBoxModel,
23
+ createModelPlacement,
24
+ } from "@vector-display/3d";
25
+ import { Angle, Vector } from "es-vector-math";
26
+
27
+ const cube = createBoxModel({ width: 100, height: 100, depth: 100 });
28
+ const projector = new WireframeProjector({
29
+ ...DEFAULT_PERSPECTIVE_CAMERA,
30
+ position: new Vector(0, 50, -300),
31
+ screenCenterX: 512,
32
+ screenCenterY: 384,
33
+ });
34
+
35
+ function addCube(displayList: DisplayList, seconds: number) {
36
+ const placement = createModelPlacement({
37
+ position: new Vector(0, 0, 400),
38
+ rotationY: Angle.fromRadians(seconds),
39
+ });
40
+ // The projector reuses its buffers: add each result before projecting the next model.
41
+ displayList.addShape(projector.project(cube, placement), SCREEN_SPACE_PLACEMENT);
42
+ }
43
+ ```
44
+
45
+ ## Concepts
46
+
47
+ - **Space:** x right, **y up**, z forward (away from the viewer). Projected output uses the display's y-down screen coordinates.
48
+ - **Models:** `WireframeModel.fromDefinition({ vertices, polylines })`, where vertices are es-vector-math `Vector`s and each polyline lists vertex indices plus whether it closes. `createBoxModel` and `createPyramidModel` are included.
49
+ - **Placement:** `createModelPlacement({ position, rotationX, rotationY, rotationZ, scale })`, applied as scale, then rotation (x, y, z), then position.
50
+ - **Camera:** uses es-vector-math's perspective model. The eye sits `viewDistance` behind a projection plane at `position`, so objects on that plane keep their size. Positive `yaw` turns toward +x; positive `pitch` tilts down.
51
+ - **Clipping:** lines closer than `nearDistance` to the eye are cut off, so objects can fly past the camera without lines stretching across the screen.
52
+
53
+ ## License
54
+
55
+ MIT © Rick Segrest
@@ -0,0 +1,12 @@
1
+ import { type Vector } from "es-vector-math";
2
+ import type { WireframeDefinition } from "./wireframeTypes.js";
3
+ export declare class WireframeModel {
4
+ readonly vertices: readonly Vector[];
5
+ readonly edgeVertexIndices: Int32Array;
6
+ readonly edgeNeighbors: Int32Array;
7
+ private constructor();
8
+ get edgeCount(): number;
9
+ static fromDefinition(definition: WireframeDefinition): WireframeModel;
10
+ }
11
+ export default WireframeModel;
12
+ //# sourceMappingURL=WireframeModel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WireframeModel.d.ts","sourceRoot":"","sources":["../src/WireframeModel.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,KAAK,EAAE,mBAAmB,EAAqB,MAAM,qBAAqB,CAAC;AAOlF,qBAAa,cAAc;IACvB,SAAgB,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAE5C,SAAgB,iBAAiB,EAAE,UAAU,CAAC;IAE9C,SAAgB,aAAa,EAAE,UAAU,CAAC;IAE1C,OAAO,eAIN;IAED,IAAW,SAAS,IAAI,MAAM,CAE7B;IAGD,OAAc,cAAc,CAAC,UAAU,EAAE,mBAAmB,GAAG,cAAc,CAS5E;CACJ;eA0Bc,cAAc"}
@@ -0,0 +1,54 @@
1
+ import { NO_NEIGHBOR_SEGMENT } from "@vector-display/core";
2
+ import { VectorMath } from "es-vector-math";
3
+ export class WireframeModel {
4
+ vertices;
5
+ // Two vertex indices per edge (start, end).
6
+ edgeVertexIndices;
7
+ // Two entries per edge: the connected previous and next edge in its polyline, or NO_NEIGHBOR_SEGMENT.
8
+ edgeNeighbors;
9
+ constructor(vertices, buffers) {
10
+ this.vertices = vertices;
11
+ this.edgeVertexIndices = Int32Array.from(buffers.vertexIndices);
12
+ this.edgeNeighbors = Int32Array.from(buffers.neighbors);
13
+ }
14
+ get edgeCount() {
15
+ return this.edgeVertexIndices.length / 2;
16
+ }
17
+ // Edges within a polyline connect at shared vertices, which lets the renderer draw clean joints.
18
+ static fromDefinition(definition) {
19
+ // es-vector-math's expand2D() resets z to 0 even for 3D vectors, so only expand 2D vertices.
20
+ const vertices = definition.vertices.map((vertex) => (vertex.dimensions === 2 ? VectorMath.expand2D(vertex) : vertex));
21
+ const buffers = { vertexIndices: [], neighbors: [] };
22
+ for (const polyline of definition.polylines) {
23
+ assertVertexIndicesExist(polyline, vertices.length);
24
+ appendPolylineEdges(buffers, polyline);
25
+ }
26
+ return new WireframeModel(vertices, buffers);
27
+ }
28
+ }
29
+ function assertVertexIndicesExist(polyline, vertexCount) {
30
+ const invalidIndex = polyline.vertexIndices.find((index) => !Number.isInteger(index) || index < 0 || index >= vertexCount);
31
+ if (invalidIndex !== undefined) {
32
+ throw new Error(`Wireframe polyline refers to vertex ${invalidIndex}, but the model has ${vertexCount} vertices`);
33
+ }
34
+ }
35
+ function appendPolylineEdges(buffers, polyline) {
36
+ const { vertexIndices, isClosed } = polyline;
37
+ if (vertexIndices.length < 2)
38
+ return;
39
+ const firstEdge = buffers.vertexIndices.length / 2;
40
+ for (let index = 0; index < vertexIndices.length - 1; index++) {
41
+ buffers.vertexIndices.push(vertexIndices[index], vertexIndices[index + 1]);
42
+ }
43
+ const wrapsAround = isClosed && vertexIndices.length > 2;
44
+ if (wrapsAround)
45
+ buffers.vertexIndices.push(vertexIndices[vertexIndices.length - 1], vertexIndices[0]);
46
+ const lastEdge = buffers.vertexIndices.length / 2 - 1;
47
+ for (let edge = firstEdge; edge <= lastEdge; edge++) {
48
+ const previousEdge = edge > firstEdge ? edge - 1 : wrapsAround ? lastEdge : NO_NEIGHBOR_SEGMENT;
49
+ const nextEdge = edge < lastEdge ? edge + 1 : wrapsAround ? firstEdge : NO_NEIGHBOR_SEGMENT;
50
+ buffers.neighbors.push(previousEdge, nextEdge);
51
+ }
52
+ }
53
+ export default WireframeModel;
54
+ //# sourceMappingURL=WireframeModel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WireframeModel.js","sourceRoot":"","sources":["../src/WireframeModel.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import { type ShapeGeometry } from "@vector-display/core";
2
+ import type { WireframeModel } from "./WireframeModel.js";
3
+ import type { ModelPlacement3D, PerspectiveCamera } from "./wireframeTypes.js";
4
+ export declare class WireframeProjector implements ShapeGeometry {
5
+ private camera;
6
+ private inverseYaw;
7
+ private inversePitch;
8
+ private coordinates;
9
+ private neighbors;
10
+ private visibleSegmentOfEdge;
11
+ private clippedEndpointFlags;
12
+ private readonly cameraSpaceVertices;
13
+ private visibleSegmentCount;
14
+ constructor(camera: PerspectiveCamera);
15
+ get segmentCoordinates(): Float32Array;
16
+ get segmentNeighbors(): Int32Array;
17
+ get segmentCount(): number;
18
+ setCamera(camera: PerspectiveCamera): void;
19
+ project(model: WireframeModel, placement: ModelPlacement3D): ShapeGeometry;
20
+ private ensureCapacity;
21
+ private transformVerticesToCameraSpace;
22
+ private writeVisibleEdges;
23
+ private linkVisibleNeighbors;
24
+ private findVisibleSegment;
25
+ private depthFromEye;
26
+ private clipToNearPlane;
27
+ private writeScreenPoint;
28
+ }
29
+ export default WireframeProjector;
30
+ //# sourceMappingURL=WireframeProjector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WireframeProjector.d.ts","sourceRoot":"","sources":["../src/WireframeProjector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE/E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAQ/E,qBAAa,kBAAmB,YAAW,aAAa;IACpD,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,WAAW,CAA+C;IAClE,OAAO,CAAC,SAAS,CAA6C;IAC9D,OAAO,CAAC,oBAAoB,CAAyC;IACrE,OAAO,CAAC,oBAAoB,CAAyC;IACrE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAgB;IACpD,OAAO,CAAC,mBAAmB,CAAK;IAEhC,YAAY,MAAM,EAAE,iBAAiB,EAIpC;IAED,IAAW,kBAAkB,IAAI,YAAY,CAE5C;IAED,IAAW,gBAAgB,IAAI,UAAU,CAExC;IAED,IAAW,YAAY,IAAI,MAAM,CAEhC;IAEM,SAAS,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAIhD;IAEM,OAAO,CAAC,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,gBAAgB,GAAG,aAAa,CAMhF;IAED,OAAO,CAAC,cAAc;IAUtB,OAAO,CAAC,8BAA8B;IAWtC,OAAO,CAAC,iBAAiB;IAuBzB,OAAO,CAAC,oBAAoB;IAoB5B,OAAO,CAAC,kBAAkB;IAI1B,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,gBAAgB;CAK3B;eAEc,kBAAkB"}
@@ -0,0 +1,124 @@
1
+ import { NO_NEIGHBOR_SEGMENT } from "@vector-display/core";
2
+ import { Angle, VectorMath } from "es-vector-math";
3
+ const START_CLIPPED = 1;
4
+ const END_CLIPPED = 2;
5
+ const INITIAL_EDGE_CAPACITY = 64;
6
+ // Projects a wireframe into screen-space line segments. The returned geometry is this projector's reusable
7
+ // buffer: add it to a display list before projecting the next model.
8
+ export class WireframeProjector {
9
+ camera;
10
+ inverseYaw;
11
+ inversePitch;
12
+ coordinates = new Float32Array(INITIAL_EDGE_CAPACITY * 4);
13
+ neighbors = new Int32Array(INITIAL_EDGE_CAPACITY * 2);
14
+ visibleSegmentOfEdge = new Int32Array(INITIAL_EDGE_CAPACITY);
15
+ clippedEndpointFlags = new Uint8Array(INITIAL_EDGE_CAPACITY);
16
+ cameraSpaceVertices = [];
17
+ visibleSegmentCount = 0;
18
+ constructor(camera) {
19
+ this.camera = camera;
20
+ this.inverseYaw = Angle.fromRadians(-camera.yaw.radians);
21
+ this.inversePitch = Angle.fromRadians(-camera.pitch.radians);
22
+ }
23
+ get segmentCoordinates() {
24
+ return this.coordinates;
25
+ }
26
+ get segmentNeighbors() {
27
+ return this.neighbors;
28
+ }
29
+ get segmentCount() {
30
+ return this.visibleSegmentCount;
31
+ }
32
+ setCamera(camera) {
33
+ this.camera = camera;
34
+ this.inverseYaw = Angle.fromRadians(-camera.yaw.radians);
35
+ this.inversePitch = Angle.fromRadians(-camera.pitch.radians);
36
+ }
37
+ project(model, placement) {
38
+ this.ensureCapacity(model.edgeCount);
39
+ this.transformVerticesToCameraSpace(model, placement);
40
+ this.writeVisibleEdges(model);
41
+ this.linkVisibleNeighbors(model);
42
+ return this;
43
+ }
44
+ ensureCapacity(edgeCount) {
45
+ if (edgeCount <= this.visibleSegmentOfEdge.length)
46
+ return;
47
+ let capacity = this.visibleSegmentOfEdge.length;
48
+ while (capacity < edgeCount)
49
+ capacity *= 2;
50
+ this.coordinates = new Float32Array(capacity * 4);
51
+ this.neighbors = new Int32Array(capacity * 2);
52
+ this.visibleSegmentOfEdge = new Int32Array(capacity);
53
+ this.clippedEndpointFlags = new Uint8Array(capacity);
54
+ }
55
+ transformVerticesToCameraSpace(model, placement) {
56
+ const { position, rotationX, rotationY, rotationZ, scale } = placement;
57
+ this.cameraSpaceVertices.length = model.vertices.length;
58
+ model.vertices.forEach((vertex, vertexIndex) => {
59
+ const rotated = VectorMath.rotateXYZ(rotationX, rotationY, rotationZ, VectorMath.scale(scale, vertex));
60
+ const relativeToCamera = VectorMath.subtract(VectorMath.add(rotated, position), this.camera.position);
61
+ const cameraSpace = VectorMath.rotateX(this.inversePitch, VectorMath.rotateY(this.inverseYaw, relativeToCamera));
62
+ this.cameraSpaceVertices[vertexIndex] = cameraSpace;
63
+ });
64
+ }
65
+ writeVisibleEdges(model) {
66
+ this.visibleSegmentCount = 0;
67
+ for (let edge = 0; edge < model.edgeCount; edge++) {
68
+ const start = this.cameraSpaceVertices[model.edgeVertexIndices[edge * 2]];
69
+ const end = this.cameraSpaceVertices[model.edgeVertexIndices[edge * 2 + 1]];
70
+ const isStartTooClose = this.depthFromEye(start) < this.camera.nearDistance;
71
+ const isEndTooClose = this.depthFromEye(end) < this.camera.nearDistance;
72
+ if (isStartTooClose && isEndTooClose) {
73
+ this.visibleSegmentOfEdge[edge] = NO_NEIGHBOR_SEGMENT;
74
+ continue;
75
+ }
76
+ const visibleStart = isStartTooClose ? this.clipToNearPlane(end, start) : start;
77
+ const visibleEnd = isEndTooClose ? this.clipToNearPlane(start, end) : end;
78
+ const segment = this.visibleSegmentCount;
79
+ this.writeScreenPoint(segment * 4, visibleStart);
80
+ this.writeScreenPoint(segment * 4 + 2, visibleEnd);
81
+ this.clippedEndpointFlags[segment] = (isStartTooClose ? START_CLIPPED : 0) | (isEndTooClose ? END_CLIPPED : 0);
82
+ this.visibleSegmentOfEdge[edge] = segment;
83
+ this.visibleSegmentCount++;
84
+ }
85
+ }
86
+ // A clipped endpoint no longer meets its neighbor, so that joint is left unconnected.
87
+ linkVisibleNeighbors(model) {
88
+ for (let edge = 0; edge < model.edgeCount; edge++) {
89
+ const segment = this.visibleSegmentOfEdge[edge];
90
+ if (segment === NO_NEIGHBOR_SEGMENT)
91
+ continue;
92
+ const previousSegment = this.findVisibleSegment(model.edgeNeighbors[edge * 2]);
93
+ const nextSegment = this.findVisibleSegment(model.edgeNeighbors[edge * 2 + 1]);
94
+ const flags = this.clippedEndpointFlags[segment];
95
+ const joinsPrevious = previousSegment !== NO_NEIGHBOR_SEGMENT &&
96
+ (flags & START_CLIPPED) === 0 &&
97
+ (this.clippedEndpointFlags[previousSegment] & END_CLIPPED) === 0;
98
+ const joinsNext = nextSegment !== NO_NEIGHBOR_SEGMENT &&
99
+ (flags & END_CLIPPED) === 0 &&
100
+ (this.clippedEndpointFlags[nextSegment] & START_CLIPPED) === 0;
101
+ this.neighbors[segment * 2] = joinsPrevious ? previousSegment : NO_NEIGHBOR_SEGMENT;
102
+ this.neighbors[segment * 2 + 1] = joinsNext ? nextSegment : NO_NEIGHBOR_SEGMENT;
103
+ }
104
+ }
105
+ findVisibleSegment(edge) {
106
+ return edge === NO_NEIGHBOR_SEGMENT ? NO_NEIGHBOR_SEGMENT : this.visibleSegmentOfEdge[edge];
107
+ }
108
+ depthFromEye(cameraSpacePoint) {
109
+ return (cameraSpacePoint.z ?? 0) + this.camera.viewDistance;
110
+ }
111
+ clipToNearPlane(visiblePoint, hiddenPoint) {
112
+ const visibleDepth = this.depthFromEye(visiblePoint);
113
+ const hiddenDepth = this.depthFromEye(hiddenPoint);
114
+ const fraction = (this.camera.nearDistance - visibleDepth) / (hiddenDepth - visibleDepth);
115
+ return VectorMath.add(visiblePoint, VectorMath.scale(fraction, VectorMath.subtract(hiddenPoint, visiblePoint)));
116
+ }
117
+ writeScreenPoint(coordinateOffset, cameraSpacePoint) {
118
+ const perspective = VectorMath.getPerspective(this.camera.viewDistance, cameraSpacePoint);
119
+ this.coordinates[coordinateOffset] = this.camera.screenCenterX + cameraSpacePoint.x * perspective;
120
+ this.coordinates[coordinateOffset + 1] = this.camera.screenCenterY - cameraSpacePoint.y * perspective;
121
+ }
122
+ }
123
+ export default WireframeProjector;
124
+ //# sourceMappingURL=WireframeProjector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WireframeProjector.js","sourceRoot":"","sources":["../src/WireframeProjector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ export { WireframeModel } from "./WireframeModel.js";
2
+ export { WireframeProjector } from "./WireframeProjector.js";
3
+ export { createBoxModel, createPyramidModel } from "./wireframePrimitives.js";
4
+ export type { BoxDimensions, PyramidDimensions } from "./wireframePrimitives.js";
5
+ export { DEFAULT_PERSPECTIVE_CAMERA, createModelPlacement } from "./wireframeTypes.js";
6
+ export type { ModelPlacement3D, PerspectiveCamera, WireframeDefinition, WireframePolyline } from "./wireframeTypes.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9E,YAAY,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACjF,OAAO,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AACvF,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { WireframeModel } from "./WireframeModel.js";
2
+ export { WireframeProjector } from "./WireframeProjector.js";
3
+ export { createBoxModel, createPyramidModel } from "./wireframePrimitives.js";
4
+ export { DEFAULT_PERSPECTIVE_CAMERA, createModelPlacement } from "./wireframeTypes.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9E,OAAO,EAAE,0BAA0B,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { WireframeModel } from "./WireframeModel.js";
2
+ export interface BoxDimensions {
3
+ readonly width: number;
4
+ readonly height: number;
5
+ readonly depth: number;
6
+ }
7
+ export interface PyramidDimensions {
8
+ readonly baseWidth: number;
9
+ readonly height: number;
10
+ }
11
+ export declare function createBoxModel(dimensions: BoxDimensions): WireframeModel;
12
+ export declare function createPyramidModel(dimensions: PyramidDimensions): WireframeModel;
13
+ //# sourceMappingURL=wireframePrimitives.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireframePrimitives.d.ts","sourceRoot":"","sources":["../src/wireframePrimitives.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,WAAW,aAAa;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CAC3B;AAGD,wBAAgB,cAAc,CAAC,UAAU,EAAE,aAAa,GAAG,cAAc,CAoBxE;AAGD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,iBAAiB,GAAG,cAAc,CAgBhF"}
@@ -0,0 +1,41 @@
1
+ import { Vector } from "es-vector-math";
2
+ import { WireframeModel } from "./WireframeModel.js";
3
+ // Centered on the origin.
4
+ export function createBoxModel(dimensions) {
5
+ const halfWidth = dimensions.width / 2;
6
+ const halfHeight = dimensions.height / 2;
7
+ const halfDepth = dimensions.depth / 2;
8
+ const vertices = [-1, 1].flatMap((ySign) => [
9
+ [-1, -1],
10
+ [1, -1],
11
+ [1, 1],
12
+ [-1, 1],
13
+ ].map(([xSign, zSign]) => new Vector(xSign * halfWidth, ySign * halfHeight, zSign * halfDepth)));
14
+ return WireframeModel.fromDefinition({
15
+ vertices,
16
+ polylines: [
17
+ { vertexIndices: [0, 1, 2, 3], isClosed: true },
18
+ { vertexIndices: [4, 5, 6, 7], isClosed: true },
19
+ ...[0, 1, 2, 3].map((corner) => ({ vertexIndices: [corner, corner + 4], isClosed: false })),
20
+ ],
21
+ });
22
+ }
23
+ // Base centered on the origin, apex pointing up (+y).
24
+ export function createPyramidModel(dimensions) {
25
+ const halfBase = dimensions.baseWidth / 2;
26
+ const vertices = [
27
+ new Vector(-halfBase, 0, -halfBase),
28
+ new Vector(halfBase, 0, -halfBase),
29
+ new Vector(halfBase, 0, halfBase),
30
+ new Vector(-halfBase, 0, halfBase),
31
+ new Vector(0, dimensions.height, 0),
32
+ ];
33
+ return WireframeModel.fromDefinition({
34
+ vertices,
35
+ polylines: [
36
+ { vertexIndices: [0, 1, 2, 3], isClosed: true },
37
+ ...[0, 1, 2, 3].map((corner) => ({ vertexIndices: [corner, 4], isClosed: false })),
38
+ ],
39
+ });
40
+ }
41
+ //# sourceMappingURL=wireframePrimitives.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireframePrimitives.js","sourceRoot":"","sources":["../src/wireframePrimitives.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AACxC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAarD,0BAA0B;AAC1B,MAAM,UAAU,cAAc,CAAC,UAAyB;IACpD,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC;IACvC,MAAM,UAAU,GAAG,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;IACzC,MAAM,SAAS,GAAG,UAAU,CAAC,KAAK,GAAG,CAAC,CAAC;IACvC,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CACvC;QACI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACR,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACP,CAAC,CAAC,EAAE,CAAC,CAAC;QACN,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;KACV,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,KAAK,GAAG,SAAS,EAAE,KAAK,GAAG,UAAU,EAAE,KAAK,GAAG,SAAS,CAAC,CAAC,CAClG,CAAC;IACF,OAAO,cAAc,CAAC,cAAc,CAAC;QACjC,QAAQ;QACR,SAAS,EAAE;YACP,EAAE,aAAa,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;YAC/C,EAAE,aAAa,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;YAC/C,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;SAC9F;KACJ,CAAC,CAAC;AACP,CAAC;AAED,sDAAsD;AACtD,MAAM,UAAU,kBAAkB,CAAC,UAA6B;IAC5D,MAAM,QAAQ,GAAG,UAAU,CAAC,SAAS,GAAG,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG;QACb,IAAI,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC;QACnC,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC,QAAQ,CAAC;QAClC,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC,EAAE,QAAQ,CAAC;QACjC,IAAI,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC,EAAE,QAAQ,CAAC;QAClC,IAAI,MAAM,CAAC,CAAC,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;KACtC,CAAC;IACF,OAAO,cAAc,CAAC,cAAc,CAAC;QACjC,QAAQ;QACR,SAAS,EAAE;YACP,EAAE,aAAa,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;YAC/C,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,aAAa,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;SACrF;KACJ,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,28 @@
1
+ import { Angle, Vector } from "es-vector-math";
2
+ export interface WireframePolyline {
3
+ readonly vertexIndices: readonly number[];
4
+ readonly isClosed: boolean;
5
+ }
6
+ export interface WireframeDefinition {
7
+ readonly vertices: readonly Vector[];
8
+ readonly polylines: readonly WireframePolyline[];
9
+ }
10
+ export interface ModelPlacement3D {
11
+ readonly position: Vector;
12
+ readonly rotationX: Angle;
13
+ readonly rotationY: Angle;
14
+ readonly rotationZ: Angle;
15
+ readonly scale: number;
16
+ }
17
+ export interface PerspectiveCamera {
18
+ readonly position: Vector;
19
+ readonly yaw: Angle;
20
+ readonly pitch: Angle;
21
+ readonly viewDistance: number;
22
+ readonly nearDistance: number;
23
+ readonly screenCenterX: number;
24
+ readonly screenCenterY: number;
25
+ }
26
+ export declare const DEFAULT_PERSPECTIVE_CAMERA: PerspectiveCamera;
27
+ export declare function createModelPlacement(overrides: Partial<ModelPlacement3D>): ModelPlacement3D;
28
+ //# sourceMappingURL=wireframeTypes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireframeTypes.d.ts","sourceRoot":"","sources":["../src/wireframeTypes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAG/C,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,CAAC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAChC,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACpD;AAGD,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CAC1B;AAID,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,QAAQ,CAAC,GAAG,EAAE,KAAK,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAE9B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;CAClC;AAED,eAAO,MAAM,0BAA0B,EAAE,iBAQxC,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,OAAO,CAAC,gBAAgB,CAAC,GAAG,gBAAgB,CAS3F"}
@@ -0,0 +1,21 @@
1
+ import { Angle, Vector } from "es-vector-math";
2
+ export const DEFAULT_PERSPECTIVE_CAMERA = {
3
+ position: new Vector(0, 0, 0),
4
+ yaw: Angle.fromRadians(0),
5
+ pitch: Angle.fromRadians(0),
6
+ viewDistance: 400,
7
+ nearDistance: 1,
8
+ screenCenterX: 512,
9
+ screenCenterY: 384,
10
+ };
11
+ export function createModelPlacement(overrides) {
12
+ return {
13
+ position: new Vector(0, 0, 0),
14
+ rotationX: Angle.fromRadians(0),
15
+ rotationY: Angle.fromRadians(0),
16
+ rotationZ: Angle.fromRadians(0),
17
+ scale: 1,
18
+ ...overrides,
19
+ };
20
+ }
21
+ //# sourceMappingURL=wireframeTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wireframeTypes.js","sourceRoot":"","sources":["../src/wireframeTypes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAoC/C,MAAM,CAAC,MAAM,0BAA0B,GAAsB;IACzD,QAAQ,EAAE,IAAI,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAC7B,GAAG,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IACzB,KAAK,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC3B,YAAY,EAAE,GAAG;IACjB,YAAY,EAAE,CAAC;IACf,aAAa,EAAE,GAAG;IAClB,aAAa,EAAE,GAAG;CACrB,CAAC;AAEF,MAAM,UAAU,oBAAoB,CAAC,SAAoC;IACrE,OAAO;QACH,QAAQ,EAAE,IAAI,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAC7B,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;QAC/B,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;QAC/B,SAAS,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;QAC/B,KAAK,EAAE,CAAC;QACR,GAAG,SAAS;KACf,CAAC;AACN,CAAC"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@vector-display/3d",
3
+ "version": "0.1.0",
4
+ "description": "Wireframe models and perspective projection for vector-display, built on es-vector-math.",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Rick Segrest",
8
+ "email": "rsegrest77@gmail.com",
9
+ "url": "https://www.ricksegrest.com"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/rsegrest/vector-display.git",
14
+ "directory": "packages/3d"
15
+ },
16
+ "homepage": "https://github.com/rsegrest/vector-display/tree/main/packages/3d#readme",
17
+ "bugs": {
18
+ "url": "https://github.com/rsegrest/vector-display/issues"
19
+ },
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "exports": {
23
+ ".": {
24
+ "@vector-display/source": "./src/index.ts",
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ }
28
+ },
29
+ "scripts": {
30
+ "prepublishOnly": "tsc -b"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src",
35
+ "!src/**/*.test.ts"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "dependencies": {
41
+ "@vector-display/core": "0.1.0",
42
+ "es-vector-math": "^0.1.0"
43
+ }
44
+ }
@@ -0,0 +1,64 @@
1
+ import { NO_NEIGHBOR_SEGMENT } from "@vector-display/core";
2
+ import { VectorMath, type Vector } from "es-vector-math";
3
+ import type { WireframeDefinition, WireframePolyline } from "./wireframeTypes.js";
4
+
5
+ interface EdgeBuffers {
6
+ readonly vertexIndices: number[];
7
+ readonly neighbors: number[];
8
+ }
9
+
10
+ export class WireframeModel {
11
+ public readonly vertices: readonly Vector[];
12
+ // Two vertex indices per edge (start, end).
13
+ public readonly edgeVertexIndices: Int32Array;
14
+ // Two entries per edge: the connected previous and next edge in its polyline, or NO_NEIGHBOR_SEGMENT.
15
+ public readonly edgeNeighbors: Int32Array;
16
+
17
+ private constructor(vertices: readonly Vector[], buffers: EdgeBuffers) {
18
+ this.vertices = vertices;
19
+ this.edgeVertexIndices = Int32Array.from(buffers.vertexIndices);
20
+ this.edgeNeighbors = Int32Array.from(buffers.neighbors);
21
+ }
22
+
23
+ public get edgeCount(): number {
24
+ return this.edgeVertexIndices.length / 2;
25
+ }
26
+
27
+ // Edges within a polyline connect at shared vertices, which lets the renderer draw clean joints.
28
+ public static fromDefinition(definition: WireframeDefinition): WireframeModel {
29
+ // es-vector-math's expand2D() resets z to 0 even for 3D vectors, so only expand 2D vertices.
30
+ const vertices = definition.vertices.map((vertex) => (vertex.dimensions === 2 ? VectorMath.expand2D(vertex) : vertex));
31
+ const buffers: EdgeBuffers = { vertexIndices: [], neighbors: [] };
32
+ for (const polyline of definition.polylines) {
33
+ assertVertexIndicesExist(polyline, vertices.length);
34
+ appendPolylineEdges(buffers, polyline);
35
+ }
36
+ return new WireframeModel(vertices, buffers);
37
+ }
38
+ }
39
+
40
+ function assertVertexIndicesExist(polyline: WireframePolyline, vertexCount: number): void {
41
+ const invalidIndex = polyline.vertexIndices.find((index) => !Number.isInteger(index) || index < 0 || index >= vertexCount);
42
+ if (invalidIndex !== undefined) {
43
+ throw new Error(`Wireframe polyline refers to vertex ${invalidIndex}, but the model has ${vertexCount} vertices`);
44
+ }
45
+ }
46
+
47
+ function appendPolylineEdges(buffers: EdgeBuffers, polyline: WireframePolyline): void {
48
+ const { vertexIndices, isClosed } = polyline;
49
+ if (vertexIndices.length < 2) return;
50
+ const firstEdge = buffers.vertexIndices.length / 2;
51
+ for (let index = 0; index < vertexIndices.length - 1; index++) {
52
+ buffers.vertexIndices.push(vertexIndices[index], vertexIndices[index + 1]);
53
+ }
54
+ const wrapsAround = isClosed && vertexIndices.length > 2;
55
+ if (wrapsAround) buffers.vertexIndices.push(vertexIndices[vertexIndices.length - 1], vertexIndices[0]);
56
+ const lastEdge = buffers.vertexIndices.length / 2 - 1;
57
+ for (let edge = firstEdge; edge <= lastEdge; edge++) {
58
+ const previousEdge = edge > firstEdge ? edge - 1 : wrapsAround ? lastEdge : NO_NEIGHBOR_SEGMENT;
59
+ const nextEdge = edge < lastEdge ? edge + 1 : wrapsAround ? firstEdge : NO_NEIGHBOR_SEGMENT;
60
+ buffers.neighbors.push(previousEdge, nextEdge);
61
+ }
62
+ }
63
+
64
+ export default WireframeModel;
@@ -0,0 +1,141 @@
1
+ import { NO_NEIGHBOR_SEGMENT, type ShapeGeometry } from "@vector-display/core";
2
+ import { Angle, VectorMath, type Vector } from "es-vector-math";
3
+ import type { WireframeModel } from "./WireframeModel.js";
4
+ import type { ModelPlacement3D, PerspectiveCamera } from "./wireframeTypes.js";
5
+
6
+ const START_CLIPPED = 1;
7
+ const END_CLIPPED = 2;
8
+ const INITIAL_EDGE_CAPACITY = 64;
9
+
10
+ // Projects a wireframe into screen-space line segments. The returned geometry is this projector's reusable
11
+ // buffer: add it to a display list before projecting the next model.
12
+ export class WireframeProjector implements ShapeGeometry {
13
+ private camera: PerspectiveCamera;
14
+ private inverseYaw: Angle;
15
+ private inversePitch: Angle;
16
+ private coordinates = new Float32Array(INITIAL_EDGE_CAPACITY * 4);
17
+ private neighbors = new Int32Array(INITIAL_EDGE_CAPACITY * 2);
18
+ private visibleSegmentOfEdge = new Int32Array(INITIAL_EDGE_CAPACITY);
19
+ private clippedEndpointFlags = new Uint8Array(INITIAL_EDGE_CAPACITY);
20
+ private readonly cameraSpaceVertices: Vector[] = [];
21
+ private visibleSegmentCount = 0;
22
+
23
+ constructor(camera: PerspectiveCamera) {
24
+ this.camera = camera;
25
+ this.inverseYaw = Angle.fromRadians(-camera.yaw.radians);
26
+ this.inversePitch = Angle.fromRadians(-camera.pitch.radians);
27
+ }
28
+
29
+ public get segmentCoordinates(): Float32Array {
30
+ return this.coordinates;
31
+ }
32
+
33
+ public get segmentNeighbors(): Int32Array {
34
+ return this.neighbors;
35
+ }
36
+
37
+ public get segmentCount(): number {
38
+ return this.visibleSegmentCount;
39
+ }
40
+
41
+ public setCamera(camera: PerspectiveCamera): void {
42
+ this.camera = camera;
43
+ this.inverseYaw = Angle.fromRadians(-camera.yaw.radians);
44
+ this.inversePitch = Angle.fromRadians(-camera.pitch.radians);
45
+ }
46
+
47
+ public project(model: WireframeModel, placement: ModelPlacement3D): ShapeGeometry {
48
+ this.ensureCapacity(model.edgeCount);
49
+ this.transformVerticesToCameraSpace(model, placement);
50
+ this.writeVisibleEdges(model);
51
+ this.linkVisibleNeighbors(model);
52
+ return this;
53
+ }
54
+
55
+ private ensureCapacity(edgeCount: number): void {
56
+ if (edgeCount <= this.visibleSegmentOfEdge.length) return;
57
+ let capacity = this.visibleSegmentOfEdge.length;
58
+ while (capacity < edgeCount) capacity *= 2;
59
+ this.coordinates = new Float32Array(capacity * 4);
60
+ this.neighbors = new Int32Array(capacity * 2);
61
+ this.visibleSegmentOfEdge = new Int32Array(capacity);
62
+ this.clippedEndpointFlags = new Uint8Array(capacity);
63
+ }
64
+
65
+ private transformVerticesToCameraSpace(model: WireframeModel, placement: ModelPlacement3D): void {
66
+ const { position, rotationX, rotationY, rotationZ, scale } = placement;
67
+ this.cameraSpaceVertices.length = model.vertices.length;
68
+ model.vertices.forEach((vertex, vertexIndex) => {
69
+ const rotated = VectorMath.rotateXYZ(rotationX, rotationY, rotationZ, VectorMath.scale(scale, vertex));
70
+ const relativeToCamera = VectorMath.subtract(VectorMath.add(rotated, position), this.camera.position);
71
+ const cameraSpace = VectorMath.rotateX(this.inversePitch, VectorMath.rotateY(this.inverseYaw, relativeToCamera));
72
+ this.cameraSpaceVertices[vertexIndex] = cameraSpace;
73
+ });
74
+ }
75
+
76
+ private writeVisibleEdges(model: WireframeModel): void {
77
+ this.visibleSegmentCount = 0;
78
+ for (let edge = 0; edge < model.edgeCount; edge++) {
79
+ const start = this.cameraSpaceVertices[model.edgeVertexIndices[edge * 2]];
80
+ const end = this.cameraSpaceVertices[model.edgeVertexIndices[edge * 2 + 1]];
81
+ const isStartTooClose = this.depthFromEye(start) < this.camera.nearDistance;
82
+ const isEndTooClose = this.depthFromEye(end) < this.camera.nearDistance;
83
+ if (isStartTooClose && isEndTooClose) {
84
+ this.visibleSegmentOfEdge[edge] = NO_NEIGHBOR_SEGMENT;
85
+ continue;
86
+ }
87
+ const visibleStart = isStartTooClose ? this.clipToNearPlane(end, start) : start;
88
+ const visibleEnd = isEndTooClose ? this.clipToNearPlane(start, end) : end;
89
+ const segment = this.visibleSegmentCount;
90
+ this.writeScreenPoint(segment * 4, visibleStart);
91
+ this.writeScreenPoint(segment * 4 + 2, visibleEnd);
92
+ this.clippedEndpointFlags[segment] = (isStartTooClose ? START_CLIPPED : 0) | (isEndTooClose ? END_CLIPPED : 0);
93
+ this.visibleSegmentOfEdge[edge] = segment;
94
+ this.visibleSegmentCount++;
95
+ }
96
+ }
97
+
98
+ // A clipped endpoint no longer meets its neighbor, so that joint is left unconnected.
99
+ private linkVisibleNeighbors(model: WireframeModel): void {
100
+ for (let edge = 0; edge < model.edgeCount; edge++) {
101
+ const segment = this.visibleSegmentOfEdge[edge];
102
+ if (segment === NO_NEIGHBOR_SEGMENT) continue;
103
+ const previousSegment = this.findVisibleSegment(model.edgeNeighbors[edge * 2]);
104
+ const nextSegment = this.findVisibleSegment(model.edgeNeighbors[edge * 2 + 1]);
105
+ const flags = this.clippedEndpointFlags[segment];
106
+ const joinsPrevious =
107
+ previousSegment !== NO_NEIGHBOR_SEGMENT &&
108
+ (flags & START_CLIPPED) === 0 &&
109
+ (this.clippedEndpointFlags[previousSegment] & END_CLIPPED) === 0;
110
+ const joinsNext =
111
+ nextSegment !== NO_NEIGHBOR_SEGMENT &&
112
+ (flags & END_CLIPPED) === 0 &&
113
+ (this.clippedEndpointFlags[nextSegment] & START_CLIPPED) === 0;
114
+ this.neighbors[segment * 2] = joinsPrevious ? previousSegment : NO_NEIGHBOR_SEGMENT;
115
+ this.neighbors[segment * 2 + 1] = joinsNext ? nextSegment : NO_NEIGHBOR_SEGMENT;
116
+ }
117
+ }
118
+
119
+ private findVisibleSegment(edge: number): number {
120
+ return edge === NO_NEIGHBOR_SEGMENT ? NO_NEIGHBOR_SEGMENT : this.visibleSegmentOfEdge[edge];
121
+ }
122
+
123
+ private depthFromEye(cameraSpacePoint: Vector): number {
124
+ return (cameraSpacePoint.z ?? 0) + this.camera.viewDistance;
125
+ }
126
+
127
+ private clipToNearPlane(visiblePoint: Vector, hiddenPoint: Vector): Vector {
128
+ const visibleDepth = this.depthFromEye(visiblePoint);
129
+ const hiddenDepth = this.depthFromEye(hiddenPoint);
130
+ const fraction = (this.camera.nearDistance - visibleDepth) / (hiddenDepth - visibleDepth);
131
+ return VectorMath.add(visiblePoint, VectorMath.scale(fraction, VectorMath.subtract(hiddenPoint, visiblePoint)));
132
+ }
133
+
134
+ private writeScreenPoint(coordinateOffset: number, cameraSpacePoint: Vector): void {
135
+ const perspective = VectorMath.getPerspective(this.camera.viewDistance, cameraSpacePoint);
136
+ this.coordinates[coordinateOffset] = this.camera.screenCenterX + cameraSpacePoint.x * perspective;
137
+ this.coordinates[coordinateOffset + 1] = this.camera.screenCenterY - cameraSpacePoint.y * perspective;
138
+ }
139
+ }
140
+
141
+ export default WireframeProjector;
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { WireframeModel } from "./WireframeModel.js";
2
+ export { WireframeProjector } from "./WireframeProjector.js";
3
+ export { createBoxModel, createPyramidModel } from "./wireframePrimitives.js";
4
+ export type { BoxDimensions, PyramidDimensions } from "./wireframePrimitives.js";
5
+ export { DEFAULT_PERSPECTIVE_CAMERA, createModelPlacement } from "./wireframeTypes.js";
6
+ export type { ModelPlacement3D, PerspectiveCamera, WireframeDefinition, WireframePolyline } from "./wireframeTypes.js";
@@ -0,0 +1,55 @@
1
+ import { Vector } from "es-vector-math";
2
+ import { WireframeModel } from "./WireframeModel.js";
3
+
4
+ export interface BoxDimensions {
5
+ readonly width: number;
6
+ readonly height: number;
7
+ readonly depth: number;
8
+ }
9
+
10
+ export interface PyramidDimensions {
11
+ readonly baseWidth: number;
12
+ readonly height: number;
13
+ }
14
+
15
+ // Centered on the origin.
16
+ export function createBoxModel(dimensions: BoxDimensions): WireframeModel {
17
+ const halfWidth = dimensions.width / 2;
18
+ const halfHeight = dimensions.height / 2;
19
+ const halfDepth = dimensions.depth / 2;
20
+ const vertices = [-1, 1].flatMap((ySign) =>
21
+ [
22
+ [-1, -1],
23
+ [1, -1],
24
+ [1, 1],
25
+ [-1, 1],
26
+ ].map(([xSign, zSign]) => new Vector(xSign * halfWidth, ySign * halfHeight, zSign * halfDepth)),
27
+ );
28
+ return WireframeModel.fromDefinition({
29
+ vertices,
30
+ polylines: [
31
+ { vertexIndices: [0, 1, 2, 3], isClosed: true },
32
+ { vertexIndices: [4, 5, 6, 7], isClosed: true },
33
+ ...[0, 1, 2, 3].map((corner) => ({ vertexIndices: [corner, corner + 4], isClosed: false })),
34
+ ],
35
+ });
36
+ }
37
+
38
+ // Base centered on the origin, apex pointing up (+y).
39
+ export function createPyramidModel(dimensions: PyramidDimensions): WireframeModel {
40
+ const halfBase = dimensions.baseWidth / 2;
41
+ const vertices = [
42
+ new Vector(-halfBase, 0, -halfBase),
43
+ new Vector(halfBase, 0, -halfBase),
44
+ new Vector(halfBase, 0, halfBase),
45
+ new Vector(-halfBase, 0, halfBase),
46
+ new Vector(0, dimensions.height, 0),
47
+ ];
48
+ return WireframeModel.fromDefinition({
49
+ vertices,
50
+ polylines: [
51
+ { vertexIndices: [0, 1, 2, 3], isClosed: true },
52
+ ...[0, 1, 2, 3].map((corner) => ({ vertexIndices: [corner, 4], isClosed: false })),
53
+ ],
54
+ });
55
+ }
@@ -0,0 +1,56 @@
1
+ import { Angle, Vector } from "es-vector-math";
2
+
3
+ // 3D space: x right, y up, z forward (away from the viewer).
4
+ export interface WireframePolyline {
5
+ readonly vertexIndices: readonly number[];
6
+ readonly isClosed: boolean;
7
+ }
8
+
9
+ export interface WireframeDefinition {
10
+ readonly vertices: readonly Vector[];
11
+ readonly polylines: readonly WireframePolyline[];
12
+ }
13
+
14
+ // Applied in order: scale, rotate (x, then y, then z, as in es-vector-math rotateXYZ), then move to position.
15
+ export interface ModelPlacement3D {
16
+ readonly position: Vector;
17
+ readonly rotationX: Angle;
18
+ readonly rotationY: Angle;
19
+ readonly rotationZ: Angle;
20
+ readonly scale: number;
21
+ }
22
+
23
+ // Uses es-vector-math's perspective model: the eye sits viewDistance behind the projection plane at position,
24
+ // so points on that plane (z = 0 relative to the camera) keep their size.
25
+ export interface PerspectiveCamera {
26
+ readonly position: Vector;
27
+ // Positive yaw turns the view toward +x; positive pitch tilts it toward -y (down).
28
+ readonly yaw: Angle;
29
+ readonly pitch: Angle;
30
+ readonly viewDistance: number;
31
+ // Lines closer to the eye than this are clipped, so objects can pass beside or through the viewer.
32
+ readonly nearDistance: number;
33
+ readonly screenCenterX: number;
34
+ readonly screenCenterY: number;
35
+ }
36
+
37
+ export const DEFAULT_PERSPECTIVE_CAMERA: PerspectiveCamera = {
38
+ position: new Vector(0, 0, 0),
39
+ yaw: Angle.fromRadians(0),
40
+ pitch: Angle.fromRadians(0),
41
+ viewDistance: 400,
42
+ nearDistance: 1,
43
+ screenCenterX: 512,
44
+ screenCenterY: 384,
45
+ };
46
+
47
+ export function createModelPlacement(overrides: Partial<ModelPlacement3D>): ModelPlacement3D {
48
+ return {
49
+ position: new Vector(0, 0, 0),
50
+ rotationX: Angle.fromRadians(0),
51
+ rotationY: Angle.fromRadians(0),
52
+ rotationZ: Angle.fromRadians(0),
53
+ scale: 1,
54
+ ...overrides,
55
+ };
56
+ }