@forgeax/engine-debug-draw 0.1.2
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 +202 -0
- package/README.md +256 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/__tests__/errors.test-d.d.ts +2 -0
- package/dist/__tests__/errors.test-d.d.ts.map +1 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/debug-draw.d.ts +65 -0
- package/dist/debug-draw.d.ts.map +1 -0
- package/dist/errors.d.ts +65 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +835 -0
- package/dist/index.mjs.map +1 -0
- package/dist/shapes/aabb.d.ts +4 -0
- package/dist/shapes/aabb.d.ts.map +1 -0
- package/dist/shapes/arrow.d.ts +11 -0
- package/dist/shapes/arrow.d.ts.map +1 -0
- package/dist/shapes/axes.d.ts +18 -0
- package/dist/shapes/axes.d.ts.map +1 -0
- package/dist/shapes/frustum.d.ts +7 -0
- package/dist/shapes/frustum.d.ts.map +1 -0
- package/dist/shapes/line.d.ts +4 -0
- package/dist/shapes/line.d.ts.map +1 -0
- package/dist/shapes/sphere.d.ts +7 -0
- package/dist/shapes/sphere.d.ts.map +1 -0
- package/dist/types.d.ts +136 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +61 -0
- package/src/__tests__/errors.test-d.ts +89 -0
- package/src/constants.ts +13 -0
- package/src/debug-draw.ts +713 -0
- package/src/errors.ts +142 -0
- package/src/index.ts +14 -0
- package/src/shapes/aabb.ts +57 -0
- package/src/shapes/arrow.ts +75 -0
- package/src/shapes/axes.ts +60 -0
- package/src/shapes/frustum.ts +182 -0
- package/src/shapes/line.ts +14 -0
- package/src/shapes/sphere.ts +74 -0
- package/src/types.ts +176 -0
package/src/errors.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// @forgeax/engine-debug-draw -- error model SSOT (feat-20260615-debug-draw M1 / w2)
|
|
2
|
+
//
|
|
3
|
+
// Closed union DebugDrawErrorCode, discriminated detail union,
|
|
4
|
+
// and structured DebugDrawError carrying .code / .expected / .hint / .detail.
|
|
5
|
+
//
|
|
6
|
+
// Decision anchors:
|
|
7
|
+
// - plan-strategy D-11: destroy-then-flush returns Result.err, shape calls no-op + warn once
|
|
8
|
+
// - requirements sec 3.6: error code closed union 4 members
|
|
9
|
+
// - AGENTS.md Error model: structured errors with .expected / .hint, never throw
|
|
10
|
+
// - architecture-principles #5 Fail Fast: validate at entry, non-conforming data never flows downstream
|
|
11
|
+
|
|
12
|
+
import { err, type Result } from '@forgeax/engine-types';
|
|
13
|
+
|
|
14
|
+
/** {@link pipeline-create-failed} payload: carries the RHI-level error detail. */
|
|
15
|
+
export interface PipelineCreateFailedDetail {
|
|
16
|
+
readonly code: 'pipeline-create-failed';
|
|
17
|
+
readonly rhiError: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** {@link buffer-allocation-failed} payload: carries the RHI-level error detail. */
|
|
21
|
+
export interface BufferAllocationFailedDetail {
|
|
22
|
+
readonly code: 'buffer-allocation-failed';
|
|
23
|
+
readonly rhiError: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** {@link flushed-after-destroy} payload: carries the instance identifier. */
|
|
27
|
+
export interface FlushedAfterDestroyDetail {
|
|
28
|
+
readonly code: 'flushed-after-destroy';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** {@link viewProj-required} payload: carries the missing parameter name. */
|
|
32
|
+
export interface ViewProjRequiredDetail {
|
|
33
|
+
readonly code: 'viewProj-required';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface DebugDrawErrorDetailByCode {
|
|
37
|
+
'pipeline-create-failed': PipelineCreateFailedDetail;
|
|
38
|
+
'buffer-allocation-failed': BufferAllocationFailedDetail;
|
|
39
|
+
'flushed-after-destroy': FlushedAfterDestroyDetail;
|
|
40
|
+
'viewProj-required': ViewProjRequiredDetail;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Closed {@link DebugDrawErrorCode} union.
|
|
45
|
+
* Exhaustive `switch (err.code)` needs no default fallback.
|
|
46
|
+
*
|
|
47
|
+
* | code | trigger |
|
|
48
|
+
* |:--|:--|
|
|
49
|
+
* | `'pipeline-create-failed'` | `device.createRenderPipeline(...)` rejected or threw |
|
|
50
|
+
* | `'buffer-allocation-failed'` | `device.createBuffer(...)` for GPU vbo allocation failed |
|
|
51
|
+
* | `'flushed-after-destroy'` | `flush()` called on an already-destroyed DebugDraw instance |
|
|
52
|
+
* | `'viewProj-required'` | `flush()` called with `undefined` or missing `viewProj` |
|
|
53
|
+
*/
|
|
54
|
+
export type DebugDrawErrorCode = keyof DebugDrawErrorDetailByCode;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Discriminated detail union for {@link DebugDrawError}, narrowed per
|
|
58
|
+
* `DebugDrawError.code`. AI users obtain the concrete shape via
|
|
59
|
+
* `switch (err.code)` without a fallback `as` cast.
|
|
60
|
+
*/
|
|
61
|
+
export type DebugDrawErrorDetail = DebugDrawErrorDetailByCode[DebugDrawErrorCode];
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Structured debug-draw error -- four-field surface
|
|
65
|
+
* (`.code` / `.expected` / `.hint` / `.detail`).
|
|
66
|
+
*
|
|
67
|
+
* AI users consume the structured triple by fields, not by parsing `.message`.
|
|
68
|
+
*/
|
|
69
|
+
type DebugDrawErrorVariant<C extends DebugDrawErrorCode> = {
|
|
70
|
+
readonly code: C;
|
|
71
|
+
readonly expected: string;
|
|
72
|
+
readonly hint: string;
|
|
73
|
+
readonly detail: DebugDrawErrorDetailByCode[C];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type DebugDrawError = {
|
|
77
|
+
[C in DebugDrawErrorCode]: DebugDrawErrorVariant<C>;
|
|
78
|
+
}[DebugDrawErrorCode];
|
|
79
|
+
|
|
80
|
+
function makeError<C extends DebugDrawErrorCode>(
|
|
81
|
+
code: C,
|
|
82
|
+
expected: string,
|
|
83
|
+
hint: string,
|
|
84
|
+
detail: DebugDrawErrorDetailByCode[C],
|
|
85
|
+
): DebugDrawErrorVariant<C> {
|
|
86
|
+
const error = {
|
|
87
|
+
code,
|
|
88
|
+
expected,
|
|
89
|
+
hint,
|
|
90
|
+
detail,
|
|
91
|
+
get message(): string {
|
|
92
|
+
return `[${code}] ${hint}`;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
return error;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Result-returning helpers consuming engine-types `Result<T, E>` + `err()`. */
|
|
99
|
+
|
|
100
|
+
export function pipelineCreateFailed(rhiError: string): Result<never, DebugDrawError> {
|
|
101
|
+
return err(
|
|
102
|
+
makeError(
|
|
103
|
+
'pipeline-create-failed',
|
|
104
|
+
'PSO creation should succeed with valid WGSL + layout',
|
|
105
|
+
`Pipeline creation failed: ${rhiError}. Check WGSL syntax, vertex layout, and depth-stencil state.`,
|
|
106
|
+
{ code: 'pipeline-create-failed', rhiError },
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function bufferAllocationFailed(rhiError: string): Result<never, DebugDrawError> {
|
|
112
|
+
return err(
|
|
113
|
+
makeError(
|
|
114
|
+
'buffer-allocation-failed',
|
|
115
|
+
'GPU vertex buffer allocation should succeed for the requested byte size',
|
|
116
|
+
`Buffer allocation failed: ${rhiError}. Check available device memory and buffer usage flags.`,
|
|
117
|
+
{ code: 'buffer-allocation-failed', rhiError },
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function flushedAfterDestroy(): Result<never, DebugDrawError> {
|
|
123
|
+
return err(
|
|
124
|
+
makeError(
|
|
125
|
+
'flushed-after-destroy',
|
|
126
|
+
'DebugDraw instance is alive and not yet destroyed',
|
|
127
|
+
'DebugDraw was destroyed; create a new instance via createDebugDraw().',
|
|
128
|
+
{ code: 'flushed-after-destroy' },
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function viewProjRequired(): Result<never, DebugDrawError> {
|
|
134
|
+
return err(
|
|
135
|
+
makeError(
|
|
136
|
+
'viewProj-required',
|
|
137
|
+
'viewProj must be provided as a Mat4 for flush to transform vertices',
|
|
138
|
+
'Pass a viewProj Mat4 to flush(encoder, view, viewProj).',
|
|
139
|
+
{ code: 'viewProj-required' },
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @forgeax/engine-debug-draw -- public barrel (feat-20260615-debug-draw-immediate-mode M1/M2)
|
|
2
|
+
//
|
|
3
|
+
// Single-entry surface: createDebugDraw factory + DebugDraw class + DebugDrawErrorCode
|
|
4
|
+
// closed union. Shape API (line / sphere / aabb / frustum) + flush are available
|
|
5
|
+
// on the returned DebugDraw instance.
|
|
6
|
+
|
|
7
|
+
export { INITIAL_VERTEX_CAPACITY, MAX_VERTEX_CAPACITY, VERTEX_STRIDE_BYTES } from './constants';
|
|
8
|
+
export { createDebugDraw, DebugDraw } from './debug-draw';
|
|
9
|
+
export type {
|
|
10
|
+
DebugDrawError,
|
|
11
|
+
DebugDrawErrorCode,
|
|
12
|
+
DebugDrawErrorDetail,
|
|
13
|
+
} from './errors';
|
|
14
|
+
export type { CreateShaderModule, DebugDrawOptions, DepthMode } from './types';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @forgeax/engine-debug-draw -- aabb shape geometry (M3 / w20)
|
|
2
|
+
//
|
|
3
|
+
// Pure geometry decomposition: 12 edges (24 vertices) from 8 corners.
|
|
4
|
+
// No staging / GPU concerns; consumed by DebugDraw class.
|
|
5
|
+
|
|
6
|
+
import type { Vec3 } from '@forgeax/engine-math';
|
|
7
|
+
|
|
8
|
+
function a(v: { readonly [index: number]: number }, i: number): number {
|
|
9
|
+
return v[i] as number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** 24 vertices forming a wireframe axis-aligned bounding box (12 edges). */
|
|
13
|
+
export function aabbVertices(min: Vec3, max: Vec3): [number, number, number][] {
|
|
14
|
+
const mnx = a(min, 0);
|
|
15
|
+
const mny = a(min, 1);
|
|
16
|
+
const mnz = a(min, 2);
|
|
17
|
+
const mxx = a(max, 0);
|
|
18
|
+
const mxy = a(max, 1);
|
|
19
|
+
const mxz = a(max, 2);
|
|
20
|
+
|
|
21
|
+
// 8 corners: x in {mnx, mxx}, y in {mny, mxy}, z in {mnz, mxz}
|
|
22
|
+
const c: [number, number, number][] = [
|
|
23
|
+
[mnx, mny, mnz], // 0
|
|
24
|
+
[mxx, mny, mnz], // 1
|
|
25
|
+
[mnx, mxy, mnz], // 2
|
|
26
|
+
[mxx, mxy, mnz], // 3
|
|
27
|
+
[mnx, mny, mxz], // 4
|
|
28
|
+
[mxx, mny, mxz], // 5
|
|
29
|
+
[mnx, mxy, mxz], // 6
|
|
30
|
+
[mxx, mxy, mxz], // 7
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// 12 edges
|
|
34
|
+
const edges: [number, number][] = [
|
|
35
|
+
[0, 1],
|
|
36
|
+
[0, 2],
|
|
37
|
+
[1, 3],
|
|
38
|
+
[2, 3],
|
|
39
|
+
[4, 5],
|
|
40
|
+
[4, 6],
|
|
41
|
+
[5, 7],
|
|
42
|
+
[6, 7],
|
|
43
|
+
[0, 4],
|
|
44
|
+
[1, 5],
|
|
45
|
+
[2, 6],
|
|
46
|
+
[3, 7],
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const result: [number, number, number][] = [];
|
|
50
|
+
for (const [ai, bi] of edges) {
|
|
51
|
+
const ac = c[ai] as [number, number, number];
|
|
52
|
+
const bc = c[bi] as [number, number, number];
|
|
53
|
+
result.push([ac[0], ac[1], ac[2]]);
|
|
54
|
+
result.push([bc[0], bc[1], bc[2]]);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @forgeax/engine-debug-draw -- arrow shape geometry (solo round 20260713-222551)
|
|
2
|
+
//
|
|
3
|
+
// Pure geometry decomposition: an arrow = 1 body segment (start->end) + 4 arrowhead
|
|
4
|
+
// segments from `end` back toward 4 canonical tip vectors, rotated so local +X aligns
|
|
5
|
+
// with the arrow's direction. Mirrors Bevy `bevy_gizmos::arrows` ArrowBuilder::drop.
|
|
6
|
+
// No staging / GPU concerns; consumed by the DebugDraw class.
|
|
7
|
+
|
|
8
|
+
import type { Vec3Like } from '@forgeax/engine-math';
|
|
9
|
+
import { quat, vec3 } from '@forgeax/engine-math';
|
|
10
|
+
|
|
11
|
+
// The 4 arrowhead tip directions in the arrow's local frame (arrow points toward +X),
|
|
12
|
+
// matching Bevy's tips array. Normalized when scaled to tipLength.
|
|
13
|
+
const TIP_DIRS: ReadonlyArray<readonly [number, number, number]> = [
|
|
14
|
+
[-1, 1, 0],
|
|
15
|
+
[-1, 0, 1],
|
|
16
|
+
[-1, -1, 0],
|
|
17
|
+
[-1, 0, -1],
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Plain Vec3Like (not a branded Vec3) — `quat.fromUnitVectors` accepts Vec3Like, so no
|
|
21
|
+
// cross-boundary brand cast is needed (brand-cast lint: casts live only in math factories).
|
|
22
|
+
const UNIT_X: Vec3Like = [1, 0, 0];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Line-segment vertices for an arrow from `start` to `end`.
|
|
26
|
+
* Returns 10 vertices = 5 segments (1 body + 4 arrowhead), each a [x,y,z] pair
|
|
27
|
+
* consumed pairwise by the line-list renderer.
|
|
28
|
+
*
|
|
29
|
+
* `tipLength` defaults to `|end - start| / 10` (Bevy's default arrowhead length).
|
|
30
|
+
* A degenerate (zero-length) arrow emits only the body segment (no orientable head).
|
|
31
|
+
*/
|
|
32
|
+
export function arrowVertices(
|
|
33
|
+
start: Vec3Like,
|
|
34
|
+
end: Vec3Like,
|
|
35
|
+
tipLength?: number,
|
|
36
|
+
): [number, number, number][] {
|
|
37
|
+
const sx = start[0] as number;
|
|
38
|
+
const sy = start[1] as number;
|
|
39
|
+
const sz = start[2] as number;
|
|
40
|
+
const ex = end[0] as number;
|
|
41
|
+
const ey = end[1] as number;
|
|
42
|
+
const ez = end[2] as number;
|
|
43
|
+
|
|
44
|
+
const verts: [number, number, number][] = [
|
|
45
|
+
[sx, sy, sz],
|
|
46
|
+
[ex, ey, ez],
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const dir = vec3.create();
|
|
50
|
+
vec3.set(dir, ex - sx, ey - sy, ez - sz);
|
|
51
|
+
const len = vec3.length(dir);
|
|
52
|
+
if (len < 1e-6) return verts; // degenerate: no orientable head
|
|
53
|
+
|
|
54
|
+
const headLen = tipLength ?? len / 10;
|
|
55
|
+
vec3.normalize(dir, dir);
|
|
56
|
+
|
|
57
|
+
// Rotate the local-frame tip vectors so +X faces the arrow direction.
|
|
58
|
+
const rot = quat.fromUnitVectors(quat.create(), UNIT_X, dir);
|
|
59
|
+
const tipLocal = vec3.create();
|
|
60
|
+
const tipWorld = vec3.create();
|
|
61
|
+
for (const [tx, ty, tz] of TIP_DIRS) {
|
|
62
|
+
vec3.set(tipLocal, tx, ty, tz);
|
|
63
|
+
vec3.normalize(tipLocal, tipLocal);
|
|
64
|
+
vec3.scale(tipLocal, tipLocal, headLen);
|
|
65
|
+
quat.transformVec3(tipWorld, rot, tipLocal);
|
|
66
|
+
// Head segment: from the arrow tip (end) back out to the rotated tip vector.
|
|
67
|
+
verts.push([ex, ey, ez]);
|
|
68
|
+
verts.push([
|
|
69
|
+
ex + (tipWorld[0] as number),
|
|
70
|
+
ey + (tipWorld[1] as number),
|
|
71
|
+
ez + (tipWorld[2] as number),
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
return verts;
|
|
75
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// @forgeax/engine-debug-draw -- axes gizmo geometry (solo round 20260713-222551)
|
|
2
|
+
//
|
|
3
|
+
// Draws a transform's local coordinate frame as three arrows (X / Y / Z), matching
|
|
4
|
+
// Bevy `gizmos.axes(transform, base_length)`. Each axis endpoint is
|
|
5
|
+
// `translation + base_length * column_i` of the world matrix — i.e. Bevy's
|
|
6
|
+
// `transform.transform_point(base_length * Vec3::AXIS)` (the world-space direction the
|
|
7
|
+
// local axis points, scale included). Colors follow the universal convention:
|
|
8
|
+
// X = red, Y = green, Z = blue. No staging / GPU concerns; consumed by the DebugDraw class.
|
|
9
|
+
|
|
10
|
+
import type { Mat4, Vec3Like } from '@forgeax/engine-math';
|
|
11
|
+
import { arrowVertices } from './arrow';
|
|
12
|
+
|
|
13
|
+
/** X=red, Y=green, Z=blue RGBA (Bevy's axes convention). */
|
|
14
|
+
export const AXES_COLORS: readonly [
|
|
15
|
+
readonly [number, number, number, number],
|
|
16
|
+
readonly [number, number, number, number],
|
|
17
|
+
readonly [number, number, number, number],
|
|
18
|
+
] = [
|
|
19
|
+
[1, 0, 0, 1],
|
|
20
|
+
[0, 1, 0, 1],
|
|
21
|
+
[0, 0, 1, 1],
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The three axis arrows for a world transform, as `{ vertices, color }` per axis
|
|
26
|
+
* (X, Y, Z). The caller (DebugDraw.axes) pushes each with its color. `worldMat` is a
|
|
27
|
+
* 16-float column-major mat4; the arrows originate at its translation (col 3) and point
|
|
28
|
+
* `length` along its local X / Y / Z (columns 0 / 1 / 2, scale included, matching Bevy).
|
|
29
|
+
*/
|
|
30
|
+
export function axesArrowSets(
|
|
31
|
+
worldMat: Mat4,
|
|
32
|
+
length: number,
|
|
33
|
+
): { vertices: [number, number, number][]; color: readonly [number, number, number, number] }[] {
|
|
34
|
+
const m = worldMat as unknown as ArrayLike<number>;
|
|
35
|
+
const ox = m[12] as number;
|
|
36
|
+
const oy = m[13] as number;
|
|
37
|
+
const oz = m[14] as number;
|
|
38
|
+
// Plain Vec3Like tuples (not branded Vec3) — arrowVertices accepts Vec3Like, so no
|
|
39
|
+
// cross-boundary brand cast is needed (brand-cast lint: casts live only in math factories).
|
|
40
|
+
const origin: Vec3Like = [ox, oy, oz];
|
|
41
|
+
|
|
42
|
+
// Columns 0/1/2 are the local X/Y/Z basis in world space (rotation * scale).
|
|
43
|
+
const cols: ReadonlyArray<readonly [number, number, number]> = [
|
|
44
|
+
[m[0] as number, m[1] as number, m[2] as number],
|
|
45
|
+
[m[4] as number, m[5] as number, m[6] as number],
|
|
46
|
+
[m[8] as number, m[9] as number, m[10] as number],
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const sets: {
|
|
50
|
+
vertices: [number, number, number][];
|
|
51
|
+
color: readonly [number, number, number, number];
|
|
52
|
+
}[] = [];
|
|
53
|
+
for (let i = 0; i < 3; i++) {
|
|
54
|
+
const c = cols[i] as readonly [number, number, number];
|
|
55
|
+
const color = AXES_COLORS[i] as readonly [number, number, number, number];
|
|
56
|
+
const end: Vec3Like = [ox + c[0] * length, oy + c[1] * length, oz + c[2] * length];
|
|
57
|
+
sets.push({ vertices: arrowVertices(origin, end), color });
|
|
58
|
+
}
|
|
59
|
+
return sets;
|
|
60
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// @forgeax/engine-debug-draw -- frustum shape geometry (M3 / w21)
|
|
2
|
+
//
|
|
3
|
+
// Pure geometry decomposition: invert viewProj matrix, transform 8 NDC
|
|
4
|
+
// corners to world space, draw 12 edges (24 vertices).
|
|
5
|
+
// Returns null when the viewProj is near-singular (determinant ~ 0).
|
|
6
|
+
// No staging / GPU concerns; consumed by DebugDraw class.
|
|
7
|
+
|
|
8
|
+
import { type Mat4, mat4 } from '@forgeax/engine-math';
|
|
9
|
+
|
|
10
|
+
function at(m: { readonly [index: number]: number }, i: number): number {
|
|
11
|
+
return m[i] as number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate 24 vertices (12 edges) for a frustum wireframe from a view-projection matrix.
|
|
16
|
+
* Returns null if the matrix is near-singular (|det| < 1e-10).
|
|
17
|
+
*/
|
|
18
|
+
export function frustumVertices(viewProj: Mat4): [number, number, number][] | null {
|
|
19
|
+
const m = viewProj;
|
|
20
|
+
|
|
21
|
+
// 4x4 determinant
|
|
22
|
+
const det =
|
|
23
|
+
at(m, 0) *
|
|
24
|
+
(at(m, 5) * (at(m, 10) * at(m, 15) - at(m, 14) * at(m, 11)) -
|
|
25
|
+
at(m, 9) * (at(m, 6) * at(m, 15) - at(m, 14) * at(m, 7)) +
|
|
26
|
+
at(m, 13) * (at(m, 6) * at(m, 11) - at(m, 10) * at(m, 7))) -
|
|
27
|
+
at(m, 4) *
|
|
28
|
+
(at(m, 1) * (at(m, 10) * at(m, 15) - at(m, 14) * at(m, 11)) -
|
|
29
|
+
at(m, 9) * (at(m, 2) * at(m, 15) - at(m, 14) * at(m, 3)) +
|
|
30
|
+
at(m, 13) * (at(m, 2) * at(m, 11) - at(m, 10) * at(m, 3))) +
|
|
31
|
+
at(m, 8) *
|
|
32
|
+
(at(m, 1) * (at(m, 6) * at(m, 15) - at(m, 14) * at(m, 7)) -
|
|
33
|
+
at(m, 5) * (at(m, 2) * at(m, 15) - at(m, 14) * at(m, 3)) +
|
|
34
|
+
at(m, 13) * (at(m, 2) * at(m, 7) - at(m, 6) * at(m, 3))) -
|
|
35
|
+
at(m, 12) *
|
|
36
|
+
(at(m, 1) * (at(m, 6) * at(m, 11) - at(m, 10) * at(m, 7)) -
|
|
37
|
+
at(m, 5) * (at(m, 2) * at(m, 11) - at(m, 10) * at(m, 3)) +
|
|
38
|
+
at(m, 9) * (at(m, 2) * at(m, 7) - at(m, 6) * at(m, 3)));
|
|
39
|
+
|
|
40
|
+
if (Math.abs(det) < 1e-10) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 4x4 inverse via cofactor expansion
|
|
45
|
+
const invDet = 1.0 / det;
|
|
46
|
+
const inv = mat4.create();
|
|
47
|
+
inv[0] =
|
|
48
|
+
(at(m, 5) * (at(m, 10) * at(m, 15) - at(m, 14) * at(m, 11)) -
|
|
49
|
+
at(m, 9) * (at(m, 6) * at(m, 15) - at(m, 14) * at(m, 7)) +
|
|
50
|
+
at(m, 13) * (at(m, 6) * at(m, 11) - at(m, 10) * at(m, 7))) *
|
|
51
|
+
invDet;
|
|
52
|
+
inv[1] =
|
|
53
|
+
-(
|
|
54
|
+
at(m, 1) * (at(m, 10) * at(m, 15) - at(m, 14) * at(m, 11)) -
|
|
55
|
+
at(m, 9) * (at(m, 2) * at(m, 15) - at(m, 14) * at(m, 3)) +
|
|
56
|
+
at(m, 13) * (at(m, 2) * at(m, 11) - at(m, 10) * at(m, 3))
|
|
57
|
+
) * invDet;
|
|
58
|
+
inv[2] =
|
|
59
|
+
(at(m, 1) * (at(m, 6) * at(m, 15) - at(m, 14) * at(m, 7)) -
|
|
60
|
+
at(m, 5) * (at(m, 2) * at(m, 15) - at(m, 14) * at(m, 3)) +
|
|
61
|
+
at(m, 13) * (at(m, 2) * at(m, 7) - at(m, 6) * at(m, 3))) *
|
|
62
|
+
invDet;
|
|
63
|
+
inv[3] =
|
|
64
|
+
-(
|
|
65
|
+
at(m, 1) * (at(m, 6) * at(m, 11) - at(m, 10) * at(m, 7)) -
|
|
66
|
+
at(m, 5) * (at(m, 2) * at(m, 11) - at(m, 10) * at(m, 3)) +
|
|
67
|
+
at(m, 9) * (at(m, 2) * at(m, 7) - at(m, 6) * at(m, 3))
|
|
68
|
+
) * invDet;
|
|
69
|
+
inv[4] =
|
|
70
|
+
-(
|
|
71
|
+
at(m, 4) * (at(m, 10) * at(m, 15) - at(m, 14) * at(m, 11)) -
|
|
72
|
+
at(m, 8) * (at(m, 6) * at(m, 15) - at(m, 14) * at(m, 7)) +
|
|
73
|
+
at(m, 12) * (at(m, 6) * at(m, 11) - at(m, 10) * at(m, 7))
|
|
74
|
+
) * invDet;
|
|
75
|
+
inv[5] =
|
|
76
|
+
(at(m, 0) * (at(m, 10) * at(m, 15) - at(m, 14) * at(m, 11)) -
|
|
77
|
+
at(m, 8) * (at(m, 2) * at(m, 15) - at(m, 14) * at(m, 3)) +
|
|
78
|
+
at(m, 12) * (at(m, 2) * at(m, 11) - at(m, 10) * at(m, 3))) *
|
|
79
|
+
invDet;
|
|
80
|
+
inv[6] =
|
|
81
|
+
-(
|
|
82
|
+
at(m, 0) * (at(m, 6) * at(m, 15) - at(m, 14) * at(m, 7)) -
|
|
83
|
+
at(m, 4) * (at(m, 2) * at(m, 15) - at(m, 14) * at(m, 3)) +
|
|
84
|
+
at(m, 12) * (at(m, 2) * at(m, 7) - at(m, 6) * at(m, 3))
|
|
85
|
+
) * invDet;
|
|
86
|
+
inv[7] =
|
|
87
|
+
(at(m, 0) * (at(m, 6) * at(m, 11) - at(m, 10) * at(m, 7)) -
|
|
88
|
+
at(m, 4) * (at(m, 2) * at(m, 11) - at(m, 10) * at(m, 3)) +
|
|
89
|
+
at(m, 8) * (at(m, 2) * at(m, 7) - at(m, 6) * at(m, 3))) *
|
|
90
|
+
invDet;
|
|
91
|
+
inv[8] =
|
|
92
|
+
(at(m, 4) * (at(m, 9) * at(m, 15) - at(m, 13) * at(m, 11)) -
|
|
93
|
+
at(m, 8) * (at(m, 5) * at(m, 15) - at(m, 13) * at(m, 7)) +
|
|
94
|
+
at(m, 12) * (at(m, 5) * at(m, 11) - at(m, 9) * at(m, 7))) *
|
|
95
|
+
invDet;
|
|
96
|
+
inv[9] =
|
|
97
|
+
-(
|
|
98
|
+
at(m, 0) * (at(m, 9) * at(m, 15) - at(m, 13) * at(m, 11)) -
|
|
99
|
+
at(m, 8) * (at(m, 1) * at(m, 15) - at(m, 13) * at(m, 3)) +
|
|
100
|
+
at(m, 12) * (at(m, 1) * at(m, 11) - at(m, 9) * at(m, 3))
|
|
101
|
+
) * invDet;
|
|
102
|
+
inv[10] =
|
|
103
|
+
(at(m, 0) * (at(m, 5) * at(m, 15) - at(m, 13) * at(m, 7)) -
|
|
104
|
+
at(m, 4) * (at(m, 1) * at(m, 15) - at(m, 13) * at(m, 3)) +
|
|
105
|
+
at(m, 12) * (at(m, 1) * at(m, 7) - at(m, 5) * at(m, 3))) *
|
|
106
|
+
invDet;
|
|
107
|
+
inv[11] =
|
|
108
|
+
-(
|
|
109
|
+
at(m, 0) * (at(m, 5) * at(m, 11) - at(m, 9) * at(m, 7)) -
|
|
110
|
+
at(m, 4) * (at(m, 1) * at(m, 11) - at(m, 9) * at(m, 3)) +
|
|
111
|
+
at(m, 8) * (at(m, 1) * at(m, 7) - at(m, 5) * at(m, 3))
|
|
112
|
+
) * invDet;
|
|
113
|
+
inv[12] =
|
|
114
|
+
-(
|
|
115
|
+
at(m, 4) * (at(m, 9) * at(m, 14) - at(m, 13) * at(m, 10)) -
|
|
116
|
+
at(m, 8) * (at(m, 5) * at(m, 14) - at(m, 13) * at(m, 6)) +
|
|
117
|
+
at(m, 12) * (at(m, 5) * at(m, 10) - at(m, 9) * at(m, 6))
|
|
118
|
+
) * invDet;
|
|
119
|
+
inv[13] =
|
|
120
|
+
(at(m, 0) * (at(m, 9) * at(m, 14) - at(m, 13) * at(m, 10)) -
|
|
121
|
+
at(m, 8) * (at(m, 1) * at(m, 14) - at(m, 13) * at(m, 2)) +
|
|
122
|
+
at(m, 12) * (at(m, 1) * at(m, 10) - at(m, 9) * at(m, 2))) *
|
|
123
|
+
invDet;
|
|
124
|
+
inv[14] =
|
|
125
|
+
-(
|
|
126
|
+
at(m, 0) * (at(m, 5) * at(m, 14) - at(m, 13) * at(m, 6)) -
|
|
127
|
+
at(m, 4) * (at(m, 1) * at(m, 14) - at(m, 13) * at(m, 2)) +
|
|
128
|
+
at(m, 12) * (at(m, 1) * at(m, 6) - at(m, 5) * at(m, 2))
|
|
129
|
+
) * invDet;
|
|
130
|
+
inv[15] =
|
|
131
|
+
(at(m, 0) * (at(m, 5) * at(m, 10) - at(m, 9) * at(m, 6)) -
|
|
132
|
+
at(m, 4) * (at(m, 1) * at(m, 10) - at(m, 9) * at(m, 2)) +
|
|
133
|
+
at(m, 8) * (at(m, 1) * at(m, 6) - at(m, 5) * at(m, 2))) *
|
|
134
|
+
invDet;
|
|
135
|
+
|
|
136
|
+
// 8 NDC corners: x,y in {-1,1}, z in [0,1] (WebGPU NDC, matches mat4.perspective).
|
|
137
|
+
// Near plane = z=0, far plane = z=1.
|
|
138
|
+
const ndc: [number, number, number, number][] = [
|
|
139
|
+
[-1, -1, 0, 1],
|
|
140
|
+
[1, -1, 0, 1],
|
|
141
|
+
[-1, 1, 0, 1],
|
|
142
|
+
[1, 1, 0, 1],
|
|
143
|
+
[-1, -1, 1, 1],
|
|
144
|
+
[1, -1, 1, 1],
|
|
145
|
+
[-1, 1, 1, 1],
|
|
146
|
+
[1, 1, 1, 1],
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
const corners: [number, number, number][] = ndc.map(([nx, ny, nz, nw]) => {
|
|
150
|
+
const cx = at(inv, 0) * nx + at(inv, 4) * ny + at(inv, 8) * nz + at(inv, 12) * nw;
|
|
151
|
+
const cy = at(inv, 1) * nx + at(inv, 5) * ny + at(inv, 9) * nz + at(inv, 13) * nw;
|
|
152
|
+
const cz = at(inv, 2) * nx + at(inv, 6) * ny + at(inv, 10) * nz + at(inv, 14) * nw;
|
|
153
|
+
const cw = at(inv, 3) * nx + at(inv, 7) * ny + at(inv, 11) * nz + at(inv, 15) * nw;
|
|
154
|
+
const iw = 1.0 / cw;
|
|
155
|
+
return [cx * iw, cy * iw, cz * iw] as [number, number, number];
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// 12 edges
|
|
159
|
+
const edges: [number, number][] = [
|
|
160
|
+
[0, 1],
|
|
161
|
+
[0, 2],
|
|
162
|
+
[1, 3],
|
|
163
|
+
[2, 3],
|
|
164
|
+
[4, 5],
|
|
165
|
+
[4, 6],
|
|
166
|
+
[5, 7],
|
|
167
|
+
[6, 7],
|
|
168
|
+
[0, 4],
|
|
169
|
+
[1, 5],
|
|
170
|
+
[2, 6],
|
|
171
|
+
[3, 7],
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
const result: [number, number, number][] = [];
|
|
175
|
+
for (const [ai, bi] of edges) {
|
|
176
|
+
const ac = corners[ai] as [number, number, number];
|
|
177
|
+
const bc = corners[bi] as [number, number, number];
|
|
178
|
+
result.push([ac[0], ac[1], ac[2]]);
|
|
179
|
+
result.push([bc[0], bc[1], bc[2]]);
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @forgeax/engine-debug-draw -- line shape geometry (M3 / w20)
|
|
2
|
+
//
|
|
3
|
+
// Pure geometry decomposition: 1 segment = 2 vertices.
|
|
4
|
+
// No staging / GPU concerns; consumed by DebugDraw class.
|
|
5
|
+
|
|
6
|
+
import type { Vec3 } from '@forgeax/engine-math';
|
|
7
|
+
|
|
8
|
+
/** 2 vertices forming a line segment from `a` to `b`. */
|
|
9
|
+
export function lineVertices(a: Vec3, b: Vec3): [number, number, number][] {
|
|
10
|
+
return [
|
|
11
|
+
[a[0] as number, a[1] as number, a[2] as number],
|
|
12
|
+
[b[0] as number, b[1] as number, b[2] as number],
|
|
13
|
+
];
|
|
14
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @forgeax/engine-debug-draw -- sphere shape geometry (M3 / w21)
|
|
2
|
+
//
|
|
3
|
+
// Pure geometry decomposition: 3 orthogonal great-circle rings
|
|
4
|
+
// (XY, XZ, YZ planes). Each ring has `segments` segments producing
|
|
5
|
+
// 2 vertices each. Default segments=16 gives 96 vertices (3 * 2 * 16).
|
|
6
|
+
// No staging / GPU concerns; consumed by DebugDraw class.
|
|
7
|
+
|
|
8
|
+
import type { Vec3 } from '@forgeax/engine-math';
|
|
9
|
+
|
|
10
|
+
function a(v: { readonly [index: number]: number }, i: number): number {
|
|
11
|
+
return v[i] as number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate vertices for a wireframe sphere.
|
|
16
|
+
* Returns 3 * 2 * segments vertices (pairs forming line segments).
|
|
17
|
+
*/
|
|
18
|
+
export function sphereVertices(
|
|
19
|
+
center: Vec3,
|
|
20
|
+
radius: number,
|
|
21
|
+
segments: number,
|
|
22
|
+
): [number, number, number][] {
|
|
23
|
+
const cx = a(center, 0);
|
|
24
|
+
const cy = a(center, 1);
|
|
25
|
+
const cz = a(center, 2);
|
|
26
|
+
const step = (2 * Math.PI) / segments;
|
|
27
|
+
|
|
28
|
+
const result: [number, number, number][] = [];
|
|
29
|
+
|
|
30
|
+
for (let plane = 0; plane < 3; plane++) {
|
|
31
|
+
for (let i = 0; i < segments; i++) {
|
|
32
|
+
const angle0 = i * step;
|
|
33
|
+
const angle1 = (i + 1) % segments;
|
|
34
|
+
|
|
35
|
+
let p0x: number;
|
|
36
|
+
let p0y: number;
|
|
37
|
+
let p0z: number;
|
|
38
|
+
let p1x: number;
|
|
39
|
+
let p1y: number;
|
|
40
|
+
let p1z: number;
|
|
41
|
+
|
|
42
|
+
if (plane === 0) {
|
|
43
|
+
// XY plane
|
|
44
|
+
p0x = cx + radius * Math.cos(angle0);
|
|
45
|
+
p0y = cy + radius * Math.sin(angle0);
|
|
46
|
+
p0z = cz;
|
|
47
|
+
p1x = cx + radius * Math.cos(angle1);
|
|
48
|
+
p1y = cy + radius * Math.sin(angle1);
|
|
49
|
+
p1z = cz;
|
|
50
|
+
} else if (plane === 1) {
|
|
51
|
+
// XZ plane
|
|
52
|
+
p0x = cx + radius * Math.cos(angle0);
|
|
53
|
+
p0y = cy;
|
|
54
|
+
p0z = cz + radius * Math.sin(angle0);
|
|
55
|
+
p1x = cx + radius * Math.cos(angle1);
|
|
56
|
+
p1y = cy;
|
|
57
|
+
p1z = cz + radius * Math.sin(angle1);
|
|
58
|
+
} else {
|
|
59
|
+
// YZ plane
|
|
60
|
+
p0x = cx;
|
|
61
|
+
p0y = cy + radius * Math.cos(angle0);
|
|
62
|
+
p0z = cz + radius * Math.sin(angle0);
|
|
63
|
+
p1x = cx;
|
|
64
|
+
p1y = cy + radius * Math.cos(angle1);
|
|
65
|
+
p1z = cz + radius * Math.sin(angle1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
result.push([p0x, p0y, p0z]);
|
|
69
|
+
result.push([p1x, p1y, p1z]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return result;
|
|
74
|
+
}
|