vgpu-render 0.1.0 → 0.2.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.
Files changed (42) hide show
  1. package/README.md +76 -26
  2. package/dist/curve.d.ts +1 -1
  3. package/dist/debug-draw-3d.d.ts +3 -3
  4. package/dist/debug-draw-3d.js +10 -10
  5. package/dist/{frame-recorder-2d.d.ts → frame-draw-list-2d.d.ts} +11 -11
  6. package/dist/{frame-recorder-2d.js → frame-draw-list-2d.js} +6 -6
  7. package/dist/{frame-recorder.d.ts → frame-draw-list.d.ts} +60 -8
  8. package/dist/{frame-recorder.js → frame-draw-list.js} +51 -10
  9. package/dist/geometry-generators.d.ts +2 -2
  10. package/dist/geometry-generators.js +1 -1
  11. package/dist/gpu-debug-draw-2d.d.ts +1 -1
  12. package/dist/gpu-debug-draw-3d.d.ts +1 -1
  13. package/dist/gpu-debug-draw-3d.js +4 -2
  14. package/dist/gpu-text-3d.d.ts +1 -1
  15. package/dist/gpu-text-3d.js +49 -2
  16. package/dist/index.d.ts +3 -3
  17. package/dist/index.js +2 -3
  18. package/dist/mesh-batch-3d.d.ts +1 -1
  19. package/dist/shape2d-shader.js +3 -2
  20. package/dist/text-atlas.d.ts +6 -4
  21. package/dist/text-atlas.js +9 -6
  22. package/dist/text-font.d.ts +48 -7
  23. package/dist/text-font.js +63 -11
  24. package/dist/text-layout.d.ts +63 -4
  25. package/dist/text-layout.js +58 -8
  26. package/dist/text-msdf-baked.d.ts +9 -4
  27. package/dist/text-msdf-baked.js +9 -4
  28. package/dist/text-msdf-font.d.ts +10 -4
  29. package/dist/text-msdf-font.js +9 -3
  30. package/dist/vector-math.d.ts +19 -0
  31. package/dist/vector-math.js +37 -0
  32. package/package.json +1 -1
  33. package/dist/layout-3d.d.ts +0 -227
  34. package/dist/layout-3d.js +0 -226
  35. package/dist/scene-examples.d.ts +0 -39
  36. package/dist/scene-examples.js +0 -200
  37. package/dist/scenes-2d.d.ts +0 -18
  38. package/dist/scenes-2d.js +0 -160
  39. package/dist/scenes-3d.d.ts +0 -46
  40. package/dist/scenes-3d.js +0 -1994
  41. package/dist/text-scenes.d.ts +0 -85
  42. package/dist/text-scenes.js +0 -338
package/dist/scenes-3d.js DELETED
@@ -1,1994 +0,0 @@
1
- import { rotatePoint } from "./curve.js";
2
- import { buildScene2D } from "./scenes-2d.js";
3
- import { arcNormals, box, boxEdgeNormals, capsuleOutlineNormals, capsuleWire, circleNormals, circlePoints, cone, coneBaseBasis, coneSideLineNormals, coneWire, cylinder, cylinderSideLineNormals, cylinderWire, disc, rectNormals, sphere, sphereRingNormals, sphereWire, } from "./geometry-generators.js";
4
- import { ALPHA_VALUES, basisCirclePoints, circleBasis, createLayout, crossVector, DASH_VALUES, fromHsv, JOINT_VALUES, NORMAL_SAMPLE_COUNT, normalSampleColor, normalSampleWidth, normalizeVector, RADIUS_VALUES, REFERENCE_OFFSET, referenceOffset, ROTATION_SAMPLE_COUNT, rotateAroundAxis, SCENE_LAYOUTS, SEGMENT_VALUES, WIDTH_VALUES, } from "./layout-3d.js";
5
- import { textScene } from "./text-scenes.js";
6
- const WHITE = [1, 1, 1, 1];
7
- const CYAN = [0.15, 0.85, 1, 0.95];
8
- const LIME = [0.7, 1, 0.25, 1];
9
- const ORANGE = [1, 0.45, 0.2, 0.95];
10
- const VIOLET = [0.75, 0.45, 1, 1];
11
- const TEAL = [0.25, 0.95, 0.9, 1];
12
- const GOLD = [0.95, 0.75, 0.25, 1];
13
- const ORANGE_FULL = [1, 0.45, 0.2, 1];
14
- const PINK = [1, 0.3, 0.6, 1];
15
- const AXIS_RED = [1, 0, 0, 1];
16
- const AXIS_GREEN = [0, 1, 0, 1];
17
- const AXIS_BLUE = [0, 0, 1, 1];
18
- /** `draw_circle_n`: `_get_circle_normals` gives one radial normal per segment. */
19
- function drawCircleNormals(recorder, points, segments, basis, options) {
20
- recorder.lineStrip(points, { ...options, normals: circleNormals(segments, basis) }, true);
21
- }
22
- /** `draw_arc_n`: `_get_arc_normals` gives one radial normal per segment of the sweep. */
23
- function drawArcNormals(recorder, points, angle, segments, basis, options) {
24
- recorder.lineStrip(points, { ...options, normals: arcNormals(angle, segments, basis) }, false);
25
- }
26
- /** `draw_rect_n`: `_get_rect_normals` gives one corner normal per loop vertex. */
27
- function drawRectNormals(recorder, points, basis, options) {
28
- recorder.lineStrip(points, { ...options, normals: rectNormals(basis) }, true);
29
- }
30
- /** `draw_box_n`: `_get_box_line_normals` gives one normal per edge (see `boxEdgeNormals`). */
31
- function drawBoxNormals(recorder, center, basis, size, options) {
32
- const mesh = transformMeshByBasis(box(size), center, basis);
33
- const normals = boxEdgeNormals(mesh.segments, center, basis);
34
- mesh.segments.forEach(([a, b], index) => recorder.line(a, b, { ...options, normal: normals[index] }));
35
- }
36
- /** Reference `_emit_circle_s`: a fan in the circle's own basis, which `disc` builds in its XZ plane. */
37
- function circleSolidMesh(radius, segments, center, basis) {
38
- const frame = { x: basis.x, y: normalizeVector(crossVector(basis.x, basis.y)), z: basis.y };
39
- return transformMeshByBasis(disc(radius, segments), center, frame);
40
- }
41
- /** Reference arc/`draw_arc` points: `center + (basis.x*cos + basis.y*sin) * radius`, `segments + 1` samples. */
42
- function arcPoints(center, radius, angle, segments, basis) {
43
- const points = [];
44
- for (let i = 0; i <= segments; i++) {
45
- const a = angle * i / segments;
46
- const cos = Math.cos(a) * radius;
47
- const sin = Math.sin(a) * radius;
48
- points.push([
49
- center[0] + basis.x[0] * cos + basis.y[0] * sin,
50
- center[1] + basis.x[1] * cos + basis.y[1] * sin,
51
- center[2] + basis.x[2] * cos + basis.y[2] * sin,
52
- ]);
53
- }
54
- return points;
55
- }
56
- function curve(points, closed = false) {
57
- return { points, closed };
58
- }
59
- // ---------------------------------------------------------------------------
60
- // Faithful Circle/Line reference port.
61
- //
62
- // These two scenes mirror `examples/circle.gd` and `examples/line.gd` exactly:
63
- // real column/row coordinates, colors, widths, dash offsets and the rotation
64
- // tunnel basis. The capture harness translates the reference scene root by
65
- // (-10, 0, 1.5) without rescaling geometry, so the same world offset is applied
66
- // here.
67
- // ---------------------------------------------------------------------------
68
- const LINE_Y = 0.1;
69
- const LINE_BASE_WIDTH = 2;
70
- const LINE_SAMPLE_LENGTH = 1.1;
71
- const LINE_COLUMN_SPACING = 2;
72
- const LINE_ROW_SPACING = 0.78;
73
- const LINE_SAMPLE_START_Z = -0.45;
74
- const STRIP_HALF_DEPTH = 0.18;
75
- const LOOP_HALF_DEPTH = 0.25;
76
- const NORMAL_LINE_VALUES = [[0, 0, 1], [0, 0.70710677, 0.70710677], [0, 1, 0], [0, 0, -1]];
77
- const CIRCLE_Y = 0.1;
78
- const CIRCLE_RADIUS = 0.32;
79
- const CIRCLE_BASE_WIDTH = 2;
80
- const CIRCLE_SEGMENTS = 64;
81
- const CIRCLE_JOINT_SEGMENTS = 12;
82
- const CIRCLE_COLUMN_SPACING = 2;
83
- const CIRCLE_ROW_SPACING = 0.82;
84
- const CIRCLE_RADIUS_ROW_SPACING = 1.15;
85
- const CIRCLE_SAMPLE_START_Z = -0.45;
86
- const CIRCLE_ROTATION_SAMPLES = 8;
87
- const CIRCLE_TUNNEL_RADIUS = 0.42;
88
- const CIRCLE_TUNNEL_DEPTH_STEP = 0.16;
89
- const CIRCLE_TUNNEL_SCALE_STEP = 0.06;
90
- const CIRCLE_TUNNEL_TWIST_STEP = Math.PI * 0.12;
91
- const CIRCLE_DASH_SPEED = 0.6;
92
- function lineColumnX(column) {
93
- return column * LINE_COLUMN_SPACING;
94
- }
95
- function lineRowZ(row) {
96
- return LINE_SAMPLE_START_Z - row * LINE_ROW_SPACING;
97
- }
98
- function lineStart(column, row) {
99
- return referenceOffset([lineColumnX(column), LINE_Y, lineRowZ(row)]);
100
- }
101
- function lineEnd(column, row) {
102
- return referenceOffset([lineColumnX(column) + LINE_SAMPLE_LENGTH, LINE_Y, lineRowZ(row)]);
103
- }
104
- function referenceStripPoints(column, row) {
105
- const x = lineColumnX(column);
106
- const z = lineRowZ(row);
107
- return [
108
- referenceOffset([x, LINE_Y, z + STRIP_HALF_DEPTH]),
109
- referenceOffset([x + LINE_SAMPLE_LENGTH * 0.33, LINE_Y, z - STRIP_HALF_DEPTH]),
110
- referenceOffset([x + LINE_SAMPLE_LENGTH * 0.66, LINE_Y, z + STRIP_HALF_DEPTH]),
111
- referenceOffset([x + LINE_SAMPLE_LENGTH, LINE_Y, z - STRIP_HALF_DEPTH]),
112
- ];
113
- }
114
- function referenceLoopPoints(column, row) {
115
- const x = lineColumnX(column);
116
- const z = lineRowZ(row);
117
- return [
118
- referenceOffset([x, LINE_Y, z + LOOP_HALF_DEPTH]),
119
- referenceOffset([x + LINE_SAMPLE_LENGTH, LINE_Y, z + LOOP_HALF_DEPTH]),
120
- referenceOffset([x + LINE_SAMPLE_LENGTH, LINE_Y, z - LOOP_HALF_DEPTH]),
121
- referenceOffset([x, LINE_Y, z - LOOP_HALF_DEPTH]),
122
- ];
123
- }
124
- function circleColumnX(column) {
125
- return column * CIRCLE_COLUMN_SPACING + 0.5;
126
- }
127
- function circleRowSpacing(column) {
128
- return column === 1 ? CIRCLE_RADIUS_ROW_SPACING : CIRCLE_ROW_SPACING;
129
- }
130
- function circleCenterLocal(column, row) {
131
- return [circleColumnX(column), CIRCLE_Y, CIRCLE_SAMPLE_START_Z - row * circleRowSpacing(column)];
132
- }
133
- /** Raw reference layout data, exported so tests and future 3D labels share one source. */
134
- export const CIRCLE_REFERENCE = {
135
- columns: 11,
136
- columnSpacing: CIRCLE_COLUMN_SPACING,
137
- columnOffset: 0.5,
138
- rowSpacing: CIRCLE_ROW_SPACING,
139
- radiusRowSpacing: CIRCLE_RADIUS_ROW_SPACING,
140
- startZ: CIRCLE_SAMPLE_START_Z,
141
- radius: CIRCLE_RADIUS,
142
- baseWidth: CIRCLE_BASE_WIDTH,
143
- sceneOffset: REFERENCE_OFFSET,
144
- solidColor: [0.25, 0.65, 1, 0.45],
145
- columnX: circleColumnX,
146
- center: (column, row) => referenceOffset(circleCenterLocal(column, row)),
147
- };
148
- /** Raw reference layout data for the Line example. */
149
- export const LINE_REFERENCE = {
150
- columns: 7,
151
- columnSpacing: LINE_COLUMN_SPACING,
152
- rowSpacing: LINE_ROW_SPACING,
153
- startZ: LINE_SAMPLE_START_Z,
154
- length: LINE_SAMPLE_LENGTH,
155
- stripHalfDepth: STRIP_HALF_DEPTH,
156
- loopHalfDepth: LOOP_HALF_DEPTH,
157
- sceneOffset: REFERENCE_OFFSET,
158
- columnX: lineColumnX,
159
- rowZ: lineRowZ,
160
- start: lineStart,
161
- end: lineEnd,
162
- strip: referenceStripPoints,
163
- loop: referenceLoopPoints,
164
- };
165
- function lineMatrix(recorder, context) {
166
- const dashOffset = context.time * 0.35;
167
- WIDTH_VALUES.forEach((width, row) => recorder.line(lineStart(0, row), lineEnd(0, row), { color: WHITE, width }));
168
- ALPHA_VALUES.forEach((alpha, row) => recorder.line(lineStart(1, row), lineEnd(1, row), { color: [1, 0.85, 0.1, alpha], width: LINE_BASE_WIDTH }));
169
- DASH_VALUES.forEach(([dash, space], row) => recorder.line(lineStart(2, row), lineEnd(2, row), { color: LIME, width: LINE_BASE_WIDTH, dash: [dash, space, dashOffset] }));
170
- JOINT_VALUES.forEach((joint, row) => recorder.lineStrip(referenceStripPoints(3, row), { color: CYAN, width: 8, joint }, false));
171
- JOINT_VALUES.forEach((joint, row) => recorder.lineStrip(referenceLoopPoints(4, row), { color: ORANGE, width: 8, joint }, true));
172
- JOINT_VALUES.forEach((joint, row) => recorder.lineStrip(referenceStripPoints(5, row), { color: VIOLET, width: 8, joint, dash: [0.18, 0.1, dashOffset] }, false));
173
- NORMAL_LINE_VALUES.forEach((normal, row) => recorder.line(lineStart(6, row), lineEnd(6, row), { color: TEAL, width: LINE_BASE_WIDTH, normal }));
174
- }
175
- // ---------------------------------------------------------------------------
176
- // Faithful Arrow reference port (`examples/arrow.gd`).
177
- //
178
- // `draw_arrow` emits only a head (two perpendicular V strips); `draw_arrow_line`
179
- // adds a shaft. Both are reproduced here instead of using `FrameRecorder.arrow`,
180
- // which always emits a shaft and a single flat V head with different proportions.
181
- // ---------------------------------------------------------------------------
182
- const ARROW_LAYOUT = createLayout(SCENE_LAYOUTS.arrow, SCENE_LAYOUTS.arrow.baseY);
183
- const ARROW_Y = SCENE_LAYOUTS.arrow.baseY;
184
- const ARROW_LENGTH = 1.1;
185
- const ARROW_BASE_WIDTH = 2;
186
- const ARROW_JOINT_WIDTH = 8;
187
- const ARROW_HEAD_LENGTH = 0.22;
188
- const ARROW_JOINT_HEAD_LENGTH = 0.34;
189
- const ARROW_BASE_HEAD_ANGLE = Math.PI / 6;
190
- const ARROW_JOINT_HEAD_ANGLE = Math.PI / 4;
191
- const ARROW_NORMAL = [0, 0, 1];
192
- const ARROW_TUNNEL_Y = 0.32;
193
- const ARROW_TUNNEL_DEPTH_STEP = 0.16;
194
- const ARROW_TUNNEL_SCALE_STEP = 0.05;
195
- const ARROW_DIR_ANGLE_VALUES = [-Math.PI * 0.35, -Math.PI * 0.12, Math.PI * 0.12, Math.PI * 0.35];
196
- const ARROW_HEAD_LENGTH_VALUES = [0.12, 0.22, 0.34, 0.46];
197
- const ARROW_HEAD_ANGLE_VALUES = [Math.PI / 9, Math.PI / 6, Math.PI / 4, Math.PI * 0.42];
198
- /** Reference `_get_perpendicular_unit(direction)`. */
199
- function perpendicularUnit(direction) {
200
- const reference = Math.abs(direction[1]) > 0.99 ? [1, 0, 0] : [0, 1, 0];
201
- return normalizeVector(crossVector(direction, reference));
202
- }
203
- /** Reference `_get_projected_perpendicular_unit(direction, vector)`. */
204
- function projectedPerpendicularUnit(direction, vector) {
205
- const dot = direction[0] * vector[0] + direction[1] * vector[1] + direction[2] * vector[2];
206
- const perpendicular = [vector[0] - direction[0] * dot, vector[1] - direction[1] * dot, vector[2] - direction[2] * dot];
207
- const lengthSquared = perpendicular[0] ** 2 + perpendicular[1] ** 2 + perpendicular[2] ** 2;
208
- return lengthSquared <= 0 ? perpendicularUnit(direction) : normalizeVector(perpendicular);
209
- }
210
- /**
211
- * Reference `_emit_arrow`: an X made of two perpendicular V strips with the tip at
212
- * `tip` and the head axis along `dir` (`|dir|` is the head length). Normal-aware
213
- * draws collapse the per-vertex surface normals into the single `normal` option.
214
- */
215
- function drawArrowHead(recorder, tip, dir, options) {
216
- const headLength = Math.hypot(dir[0], dir[1], dir[2]);
217
- if (headLength <= 0)
218
- return;
219
- const forward = scaleVec(dir, 1 / headLength);
220
- const normalAware = options.normal !== undefined;
221
- // `_emit_arrow_with_normal` only uses the caller's normal to pick the head's tangent plane;
222
- // the segments themselves are shaded from the cone surface (see `surfaceNormal` below).
223
- const tangent = normalAware ? projectedPerpendicularUnit(forward, options.normal) : perpendicularUnit(forward);
224
- const bitangent = normalizeVector(crossVector(forward, tangent));
225
- const headAngle = Math.min(Math.max(options.headAngle, 0.001), Math.PI * 0.49);
226
- const headRadius = Math.tan(headAngle) * headLength;
227
- const headCenter = offsetAlong(tip, forward, -headLength);
228
- const wingOptions = {
229
- color: options.color,
230
- width: options.width,
231
- ...(options.widthMode === undefined ? {} : { widthMode: options.widthMode }),
232
- ...(options.joint === undefined ? {} : { joint: options.joint }),
233
- };
234
- /** `_get_arrow_surface_normal`: `normalize(radial * headLength + direction * headRadius)`. */
235
- const surfaceNormal = (radial) => normalizeVector(offsetAlong(scaleVec(radial, headLength), forward, headRadius));
236
- const wing = (radial) => {
237
- const points = [offsetAlong(headCenter, radial, headRadius), tip, offsetAlong(headCenter, radial, -headRadius)];
238
- // `_emit_normal_line_strip` carries one normal per segment, each the surface normal of the
239
- // wing that segment starts from.
240
- recorder.lineStrip(points, normalAware ? { ...wingOptions, normals: [surfaceNormal(radial), surfaceNormal(scaleVec(radial, -1))] } : wingOptions, false);
241
- };
242
- wing(tangent);
243
- wing(bitangent);
244
- }
245
- /** `_get_arrow_tip`: head-only column tips sit half a sample (a full one for `dir`) ahead. */
246
- function arrowTip(column, row) {
247
- const offset = column === 1 ? ARROW_LENGTH : ARROW_LENGTH * 0.5;
248
- return referenceOffset([ARROW_LAYOUT.columnX(column) + offset, ARROW_Y, ARROW_LAYOUT.rowZ(column, row)]);
249
- }
250
- /** `_get_arrow_dir`: `RIGHT.rotated(BACK, angle) * ARROW_HEAD_LENGTH`. */
251
- function arrowDirection(row) {
252
- return scaleVec(rotateAroundAxis([1, 0, 0], [0, 0, 1], ARROW_DIR_ANGLE_VALUES[row]), ARROW_HEAD_LENGTH);
253
- }
254
- /** `_get_line_start`: the rotation column lifts and recesses each row. */
255
- function arrowLineStart(column, row) {
256
- const local = ARROW_LAYOUT.atLocal(column, row);
257
- if (column === 9) {
258
- return referenceOffset([local[0], local[1] + ARROW_TUNNEL_Y, local[2] - row * ARROW_TUNNEL_DEPTH_STEP]);
259
- }
260
- return referenceOffset(local);
261
- }
262
- /** `_get_rotation_vector`: shrunk, then yawed around UP and pitched around BACK. */
263
- function arrowLineVector(row) {
264
- const vector = scaleVec([1, 0, 0], ARROW_LENGTH * (1 - row * ARROW_TUNNEL_SCALE_STEP));
265
- const yawed = rotateAroundAxis(vector, [0, 1, 0], row * Math.PI * 0.08);
266
- return rotateAroundAxis(yawed, [0, 0, 1], -Math.PI * 0.18 + row * Math.PI * 0.08);
267
- }
268
- function arrowLineEnd(column, row) {
269
- const start = arrowLineStart(column, row);
270
- const vector = column === 9 ? arrowLineVector(row) : [ARROW_LENGTH, 0, 0];
271
- return [start[0] + vector[0], start[1] + vector[1], start[2] + vector[2]];
272
- }
273
- /** Reference `draw_arrow_line*`: a plain shaft plus the head at `end`. */
274
- function drawArrowLine(recorder, start, end, options) {
275
- // Both `draw_arrow_line` and `draw_arrow_line_n` emit the shaft with `_emit_line`, which carries
276
- // no normal: the caller's normal only orients the head's tangent plane.
277
- recorder.line(start, end, {
278
- color: options.color,
279
- width: options.width,
280
- ...(options.widthMode === undefined ? {} : { widthMode: options.widthMode }),
281
- ...(options.dash === undefined ? {} : { dash: options.dash }),
282
- });
283
- const dx = end[0] - start[0];
284
- const dy = end[1] - start[1];
285
- const dz = end[2] - start[2];
286
- const length = Math.hypot(dx, dy, dz);
287
- if (length <= 0)
288
- return;
289
- const scale = options.headLength / length;
290
- drawArrowHead(recorder, end, [dx * scale, dy * scale, dz * scale], options);
291
- }
292
- function arrowMatrix(recorder, context) {
293
- const dashOffset = fposmod(context.time * DASH_OFFSET_SPEED, 10);
294
- const baseHead = { headAngle: ARROW_BASE_HEAD_ANGLE };
295
- const jointHead = { headAngle: ARROW_JOINT_HEAD_ANGLE };
296
- // arrow(width) / arrow(dir) / arrow(head angle) / arrow(joint)
297
- WIDTH_VALUES.forEach((width, row) => {
298
- drawArrowHead(recorder, arrowTip(0, row), [ARROW_HEAD_LENGTH, 0, 0], { color: WHITE, width, ...baseHead });
299
- });
300
- ARROW_DIR_ANGLE_VALUES.forEach((_angle, row) => {
301
- drawArrowHead(recorder, arrowTip(1, row), arrowDirection(row), { color: CYAN, width: ARROW_BASE_WIDTH, ...baseHead });
302
- });
303
- ARROW_HEAD_ANGLE_VALUES.forEach((headAngle, row) => {
304
- drawArrowHead(recorder, arrowTip(2, row), [ARROW_HEAD_LENGTH, 0, 0], { color: LIME, width: ARROW_BASE_WIDTH, headAngle });
305
- });
306
- JOINT_VALUES.forEach((joint, row) => {
307
- drawArrowHead(recorder, arrowTip(3, row), [ARROW_JOINT_HEAD_LENGTH, 0, 0], { color: VIOLET, width: ARROW_JOINT_WIDTH, ...jointHead, joint });
308
- });
309
- // arrow_n / arrow_n(joint)
310
- for (let row = 0; row < NORMAL_SAMPLE_COUNT; row++) {
311
- drawArrowHead(recorder, arrowTip(4, row), [ARROW_HEAD_LENGTH, 0, 0], { color: normalSampleColor(row), width: normalSampleWidth(row, ARROW_BASE_WIDTH), ...baseHead, normal: ARROW_NORMAL });
312
- }
313
- JOINT_VALUES.forEach((joint, row) => {
314
- drawArrowHead(recorder, arrowTip(5, row), [ARROW_JOINT_HEAD_LENGTH, 0, 0], { color: TEAL, width: ARROW_JOINT_WIDTH, ...jointHead, joint, normal: ARROW_NORMAL });
315
- });
316
- // arrow_line(width)
317
- WIDTH_VALUES.forEach((width, row) => {
318
- drawArrowLine(recorder, arrowLineStart(6, row), arrowLineEnd(6, row), { color: WHITE, width, headLength: ARROW_HEAD_LENGTH, ...baseHead });
319
- });
320
- // arrow_line(head length)
321
- ARROW_HEAD_LENGTH_VALUES.forEach((headLength, row) => {
322
- drawArrowLine(recorder, arrowLineStart(7, row), arrowLineEnd(7, row), { color: GOLD, width: ARROW_BASE_WIDTH, headLength, ...baseHead });
323
- });
324
- // arrow_line(head angle)
325
- ARROW_HEAD_ANGLE_VALUES.forEach((headAngle, row) => {
326
- drawArrowLine(recorder, arrowLineStart(8, row), arrowLineEnd(8, row), { color: VIOLET, width: ARROW_BASE_WIDTH, headLength: ARROW_HEAD_LENGTH, headAngle });
327
- });
328
- // arrow_line(rotation)
329
- for (let row = 0; row < ROTATION_SAMPLE_COUNT; row++) {
330
- drawArrowLine(recorder, arrowLineStart(9, row), arrowLineEnd(9, row), { color: [0.95, 0.75, 0.25, 1 - row * 0.07], width: ARROW_BASE_WIDTH, headLength: ARROW_HEAD_LENGTH, ...baseHead });
331
- }
332
- // arrow_line(joint)
333
- JOINT_VALUES.forEach((joint, row) => {
334
- drawArrowLine(recorder, arrowLineStart(10, row), arrowLineEnd(10, row), { color: VIOLET, width: ARROW_JOINT_WIDTH, headLength: ARROW_JOINT_HEAD_LENGTH, ...jointHead, joint });
335
- });
336
- // arrow_line_d(dash)
337
- DASH_VALUES.forEach(([dash, space], row) => {
338
- drawArrowLine(recorder, arrowLineStart(11, row), arrowLineEnd(11, row), { color: ORANGE_FULL, width: ARROW_BASE_WIDTH, headLength: ARROW_HEAD_LENGTH, ...baseHead, dash: [dash, space, fposmod(dashOffset, dash + space)] });
339
- });
340
- // arrow_line_d(joint)
341
- JOINT_VALUES.forEach((joint, row) => {
342
- drawArrowLine(recorder, arrowLineStart(12, row), arrowLineEnd(12, row), { color: PINK, width: ARROW_JOINT_WIDTH, headLength: ARROW_JOINT_HEAD_LENGTH, ...jointHead, joint, dash: [0.18, 0.1, fposmod(dashOffset, 0.28)] });
343
- });
344
- // arrow_line_n
345
- for (let row = 0; row < NORMAL_SAMPLE_COUNT; row++) {
346
- drawArrowLine(recorder, arrowLineStart(13, row), arrowLineEnd(13, row), { color: normalSampleColor(row), width: normalSampleWidth(row, ARROW_BASE_WIDTH), headLength: ARROW_HEAD_LENGTH, ...baseHead, normal: ARROW_NORMAL });
347
- }
348
- // arrow_line_n(joint)
349
- JOINT_VALUES.forEach((joint, row) => {
350
- drawArrowLine(recorder, arrowLineStart(14, row), arrowLineEnd(14, row), { color: TEAL, width: ARROW_JOINT_WIDTH, headLength: ARROW_JOINT_HEAD_LENGTH, ...jointHead, joint, normal: ARROW_NORMAL });
351
- });
352
- // arrow_line_n_d(dash)
353
- DASH_VALUES.forEach(([dash, space], row) => {
354
- drawArrowLine(recorder, arrowLineStart(15, row), arrowLineEnd(15, row), { color: TEAL, width: ARROW_BASE_WIDTH, headLength: ARROW_HEAD_LENGTH, ...baseHead, normal: ARROW_NORMAL, dash: [dash, space, fposmod(dashOffset, dash + space)] });
355
- });
356
- // arrow_line_n_d(joint)
357
- JOINT_VALUES.forEach((joint, row) => {
358
- drawArrowLine(recorder, arrowLineStart(16, row), arrowLineEnd(16, row), { color: TEAL, width: ARROW_JOINT_WIDTH, headLength: ARROW_JOINT_HEAD_LENGTH, ...jointHead, joint, normal: ARROW_NORMAL, dash: [0.18, 0.1, fposmod(dashOffset, 0.28)] });
359
- });
360
- }
361
- // ---------------------------------------------------------------------------
362
- // Faithful Rect / Box / Axes reference port.
363
- //
364
- // These three scenes mirror `examples/rect.gd`, `examples/box.gd` and
365
- // `examples/axes.gd`: real column/row coordinates, `Basis` rotations, widths,
366
- // dash offsets and normal-aware axes. `createLayout` applies the same
367
- // `(-10, 0, 1.5)` root translation the capture harness uses.
368
- // ---------------------------------------------------------------------------
369
- const DASH_OFFSET_SPEED = 0.6;
370
- const IDENTITY_BASIS = { x: [1, 0, 0], y: [0, 1, 0], z: [0, 0, 1] };
371
- function offsetAlong(origin, axis, amount) {
372
- return [origin[0] + axis[0] * amount, origin[1] + axis[1] * amount, origin[2] + axis[2] * amount];
373
- }
374
- function scaleVec(v, factor) {
375
- return [v[0] * factor, v[1] * factor, v[2] * factor];
376
- }
377
- function fposmod(value, modulus) {
378
- return ((value % modulus) + modulus) % modulus;
379
- }
380
- /** `Basis.rotated(axis, angle)` is `Basis(axis, angle) * basis`, so every column rotates. */
381
- function rotateBasis(basis, axis, angle) {
382
- return {
383
- x: rotateAroundAxis(basis.x, axis, angle),
384
- y: rotateAroundAxis(basis.y, axis, angle),
385
- z: rotateAroundAxis(basis.z, axis, angle),
386
- };
387
- }
388
- /** Places an origin-centred mesh through `Basis(basis.x, basis.y, basis.z)` at `position`. */
389
- function transformMeshByBasis(mesh, position, basis) {
390
- const apply = (p) => [
391
- position[0] + basis.x[0] * p[0] + basis.y[0] * p[1] + basis.z[0] * p[2],
392
- position[1] + basis.x[1] * p[0] + basis.y[1] * p[1] + basis.z[1] * p[2],
393
- position[2] + basis.x[2] * p[0] + basis.y[2] * p[1] + basis.z[2] * p[2],
394
- ];
395
- return {
396
- triangles: mesh.triangles.map(([a, b, c]) => [apply(a), apply(b), apply(c)]),
397
- segments: mesh.segments.map(([a, b]) => [apply(a), apply(b)]),
398
- };
399
- }
400
- // --- Rect -----------------------------------------------------------------
401
- const RECT_LAYOUT = createLayout(SCENE_LAYOUTS.rect, SCENE_LAYOUTS.rect.baseY);
402
- const RECT_WIDTH = 0.95;
403
- const RECT_HEIGHT = 0.45;
404
- const RECT_BASE_LINE_WIDTH = 2;
405
- const RECT_EMPHASIS_LINE_WIDTH = 5.5;
406
- const RECT_SIZE_VALUES = [[0.95, 0.45], [1.25, 0.45], [0.95, 0.75], [0.55, 0.55]];
407
- const RECT_ROTATION_TUNNEL_WIDTH = 1.15;
408
- const RECT_ROTATION_TUNNEL_HEIGHT = 0.7;
409
- const RECT_ROTATION_TUNNEL_DEPTH_STEP = 0.16;
410
- const RECT_ROTATION_TUNNEL_SCALE_STEP = 0.07;
411
- const RECT_ROTATION_TUNNEL_TWIST_STEP = Math.PI * 0.12;
412
- const RECT_S_SAMPLE_COUNT = 8;
413
- const RECT_S_TUNNEL_WIDTH = 1.1;
414
- const RECT_S_TUNNEL_HEIGHT = 0.65;
415
- const RECT_S_TUNNEL_DEPTH_STEP = 0.15;
416
- const RECT_S_TUNNEL_SCALE_STEP = 0.06;
417
- const RECT_S_TUNNEL_TWIST_STEP = Math.PI * 0.12;
418
- /** `_get_rect_rotation(0.0, true)`: flat rect spanning the XZ plane. */
419
- const RECT_FLAT_BASIS = { x: [1, 0, 0], y: normalizeVector(crossVector([0, 1, 0], [1, 0, 0])), z: [0, 1, 0] };
420
- /** `_get_rect_points`: four corners in the plane spanned by `basis.x`/`basis.y`. */
421
- function rectLoop(center, basis, width, height) {
422
- const halfWidth = scaleVec(basis.x, width * 0.5);
423
- const halfHeight = scaleVec(basis.y, height * 0.5);
424
- const corner = (sx, sy) => [
425
- center[0] + halfWidth[0] * sx + halfHeight[0] * sy,
426
- center[1] + halfWidth[1] * sx + halfHeight[1] * sy,
427
- center[2] + halfWidth[2] * sx + halfHeight[2] * sy,
428
- ];
429
- return [corner(-1, -1), corner(1, -1), corner(1, 1), corner(-1, 1)];
430
- }
431
- function rectMesh(center, basis, width, height) {
432
- const p = rectLoop(center, basis, width, height);
433
- return {
434
- triangles: [[p[0], p[1], p[2]], [p[0], p[2], p[3]]],
435
- segments: [[p[0], p[1]], [p[1], p[2]], [p[2], p[3]], [p[3], p[0]]],
436
- };
437
- }
438
- function rectTunnelCenter(column, row, yOffset, depthStep) {
439
- const center = RECT_LAYOUT.atLocal(column, row);
440
- return referenceOffset([center[0], center[1] + yOffset, center[2] - row * depthStep]);
441
- }
442
- function rectRotationTunnelBasis(row) {
443
- return rotateBasis(IDENTITY_BASIS, [0, 0, 1], row * RECT_ROTATION_TUNNEL_TWIST_STEP);
444
- }
445
- function rectSolidBasis(row) {
446
- const twisted = rotateBasis(IDENTITY_BASIS, [0, 0, 1], row * RECT_S_TUNNEL_TWIST_STEP);
447
- return rotateBasis(twisted, [1, 0, 0], row * 0.08);
448
- }
449
- // --- Box ------------------------------------------------------------------
450
- const BOX_LAYOUT = createLayout(SCENE_LAYOUTS.box, SCENE_LAYOUTS.box.baseY);
451
- const BOX_BASE_Y = SCENE_LAYOUTS.box.baseY;
452
- const BOX_SIZE = [0.72, 0.46, 0.46];
453
- const BOX_BASE_LINE_WIDTH = 2;
454
- const BOX_SIZE_VALUES = [[0.72, 0.46, 0.46], [1, 0.46, 0.46], [0.72, 0.72, 0.46], [0.72, 0.46, 0.76]];
455
- const BOX_ROTATION_TUNNEL_SIZE = [0.82, 0.52, 0.52];
456
- const BOX_ROTATION_TUNNEL_DEPTH_STEP = 0.16;
457
- const BOX_ROTATION_TUNNEL_SCALE_STEP = 0.05;
458
- const BOX_ROTATION_TUNNEL_Y = 0.35;
459
- const BOX_S_SAMPLE_COUNT = 8;
460
- const BOX_S_TUNNEL_SIZE = [0.82, 0.52, 0.52];
461
- const BOX_S_TUNNEL_DEPTH_STEP = 0.16;
462
- const BOX_S_TUNNEL_SCALE_STEP = 0.05;
463
- const BOX_S_TUNNEL_Y = 0.35;
464
- /** `_get_column_size`: per-column box size, including the shrinking tunnels. */
465
- function boxColumnSize(column, row) {
466
- if (column === 1)
467
- return BOX_SIZE_VALUES[row];
468
- if (column === 2)
469
- return scaleVec(BOX_ROTATION_TUNNEL_SIZE, 1 - row * BOX_ROTATION_TUNNEL_SCALE_STEP);
470
- if (column === 6)
471
- return scaleVec(BOX_S_TUNNEL_SIZE, 1 - row * BOX_S_TUNNEL_SCALE_STEP);
472
- return BOX_SIZE;
473
- }
474
- /** `_get_box_center`: boxes sit with their base at `BOX_BASE_Y`. */
475
- function boxCenter(column, row) {
476
- const size = boxColumnSize(column, row);
477
- return referenceOffset([BOX_LAYOUT.columnX(column), BOX_BASE_Y + size[1] * 0.5, BOX_LAYOUT.rowZ(column, row)]);
478
- }
479
- function boxTunnelCenter(column, row, yOffset, depthStep) {
480
- const center = boxCenter(column, row);
481
- return [center[0], center[1] + yOffset, center[2] - row * depthStep];
482
- }
483
- function boxRotationTunnelBasis(row) {
484
- let basis = rotateBasis(IDENTITY_BASIS, [1, 0, 0], -Math.PI * 0.24);
485
- basis = rotateBasis(basis, [0, 0, 1], row * Math.PI * 0.10);
486
- return rotateBasis(basis, [0, 1, 0], row * Math.PI * 0.08);
487
- }
488
- function boxSolidBasis(row) {
489
- let basis = rotateBasis(IDENTITY_BASIS, [1, 0, 0], -Math.PI * 0.20);
490
- basis = rotateBasis(basis, [0, 0, 1], row * Math.PI * 0.10);
491
- return rotateBasis(basis, [0, 1, 0], row * Math.PI * 0.08);
492
- }
493
- function drawBox(recorder, center, basis, size, options) {
494
- const mesh = transformMeshByBasis(box(size), center, basis);
495
- for (const [a, b] of mesh.segments)
496
- recorder.line(a, b, options);
497
- }
498
- // --- Axes -----------------------------------------------------------------
499
- const AXES_LAYOUT = createLayout(SCENE_LAYOUTS.axes, SCENE_LAYOUTS.axes.baseY);
500
- const AXES_LENGTH = 0.75;
501
- const AXES_BASE_WIDTH = 2;
502
- const AXES_LENGTH_VALUES = [0.35, 0.55, 0.75, 1];
503
- const AXES_ROTATION_TUNNEL_LENGTH = 0.9;
504
- const AXES_ROTATION_TUNNEL_DEPTH_STEP = 0.16;
505
- const AXES_ROTATION_TUNNEL_SCALE_STEP = 0.05;
506
- const AXES_ROTATION_TUNNEL_Y = 0.35;
507
- function axesTunnelPos(column, row) {
508
- const pos = AXES_LAYOUT.atLocal(column, row);
509
- return referenceOffset([pos[0], pos[1] + AXES_ROTATION_TUNNEL_Y, pos[2] - row * AXES_ROTATION_TUNNEL_DEPTH_STEP]);
510
- }
511
- function axesRotationTunnelBasis(row) {
512
- let basis = rotateBasis(IDENTITY_BASIS, [1, 0, 0], -Math.PI * 0.26);
513
- basis = rotateBasis(basis, [0, 0, 1], row * Math.PI * 0.11);
514
- return rotateBasis(basis, [0, 1, 0], row * Math.PI * 0.08);
515
- }
516
- function drawAxes(recorder, origin, basis, length, width, widthMode) {
517
- const mode = widthMode === undefined ? {} : { widthMode };
518
- recorder.line(origin, offsetAlong(origin, basis.x, length), { color: AXIS_RED, width, ...mode });
519
- recorder.line(origin, offsetAlong(origin, basis.y, length), { color: AXIS_GREEN, width, ...mode });
520
- recorder.line(origin, offsetAlong(origin, basis.z, length), { color: AXIS_BLUE, width, ...mode });
521
- }
522
- function drawAxesN(recorder, origin, basis, length, width, widthMode) {
523
- const mode = widthMode === undefined ? {} : { widthMode };
524
- recorder.line(origin, offsetAlong(origin, basis.x, length), { color: AXIS_RED, width, normal: basis.x, ...mode });
525
- recorder.line(origin, offsetAlong(origin, basis.y, length), { color: AXIS_GREEN, width, normal: basis.y, ...mode });
526
- recorder.line(origin, offsetAlong(origin, basis.z, length), { color: AXIS_BLUE, width, normal: basis.z, ...mode });
527
- }
528
- function axesMatrix(recorder, context) {
529
- AXES_LENGTH_VALUES.forEach((length, row) => {
530
- drawAxes(recorder, AXES_LAYOUT.at(0, row), IDENTITY_BASIS, length, AXES_BASE_WIDTH);
531
- });
532
- WIDTH_VALUES.forEach((width, row) => {
533
- drawAxes(recorder, AXES_LAYOUT.at(1, row), IDENTITY_BASIS, AXES_LENGTH, width);
534
- });
535
- for (let row = 0; row < ROTATION_SAMPLE_COUNT; row++) {
536
- const scale = 1 - row * AXES_ROTATION_TUNNEL_SCALE_STEP;
537
- drawAxes(recorder, axesTunnelPos(2, row), axesRotationTunnelBasis(row), AXES_ROTATION_TUNNEL_LENGTH * scale, AXES_BASE_WIDTH);
538
- }
539
- AXES_LENGTH_VALUES.forEach((length, row) => {
540
- drawAxesN(recorder, AXES_LAYOUT.at(3, row), IDENTITY_BASIS, length, AXES_BASE_WIDTH);
541
- });
542
- WIDTH_VALUES.forEach((width, row) => {
543
- drawAxesN(recorder, AXES_LAYOUT.at(4, row), IDENTITY_BASIS, AXES_LENGTH, width);
544
- });
545
- for (let row = 0; row < ROTATION_SAMPLE_COUNT; row++) {
546
- const scale = 1 - row * AXES_ROTATION_TUNNEL_SCALE_STEP;
547
- drawAxesN(recorder, axesTunnelPos(5, row), axesRotationTunnelBasis(row), AXES_ROTATION_TUNNEL_LENGTH * scale, AXES_BASE_WIDTH);
548
- }
549
- void context;
550
- }
551
- // ---------------------------------------------------------------------------
552
- // Faithful Grid reference port (`examples/grid.gd`).
553
- //
554
- // Unlike every other scene, the grid columns are laid out from their own
555
- // measured bounds: `_get_column_x` chains half widths and `COLUMN_GAP`, while
556
- // `_get_row_z` stacks each row's z size plus `SAMPLE_SPACING_GAP`. The bounds
557
- // math is ported from `_get_grid_bounds`; the rendered `skew` and `outer_edges`
558
- // reuse the library grid (see the notes in `the parity notes`).
559
- // ---------------------------------------------------------------------------
560
- const GRID_SCENE_Y = 0.06;
561
- const GRID_SCENE_BASE_LINE_WIDTH = 2;
562
- const GRID_SCENE_CELL_COUNT = [6, 4];
563
- const GRID_SCENE_SPACING = [0.28, 0.28];
564
- const GRID_SCENE_START_Z = -0.45;
565
- const GRID_SCENE_COLUMN_GAP = 1;
566
- const GRID_SCENE_SAMPLE_GAP = 0.55;
567
- const GRID_SCENE_WIDTH_VALUES = WIDTH_VALUES;
568
- const GRID_SCENE_CELL_VALUES = [[4, 4], [6, 4], [10, 3], [3, 8]];
569
- const GRID_SCENE_SPACING_VALUES = [[0.2, 0.2], [0.28, 0.28], [0.42, 0.22], [0.55, 0.28]];
570
- const GRID_SCENE_SKEW_VALUES = [[0, 0], [0.28, 0], [-0.28, 0], [0.28, -0.18]];
571
- const GRID_SCENE_OUTER_VALUES = ["none", "x", "z", "xz"];
572
- const GRID_SCENE_ROTATION_COUNT = 6;
573
- const GRID_SCENE_COLOR_COUNT = 4;
574
- const GRID_SCENE_NORMAL_COUNT = 4;
575
- function gridSceneCellCount(column, row) {
576
- return column === 1 ? GRID_SCENE_CELL_VALUES[row] : GRID_SCENE_CELL_COUNT;
577
- }
578
- function gridSceneSpacing(column, row) {
579
- return column === 2 ? GRID_SCENE_SPACING_VALUES[row] : GRID_SCENE_SPACING;
580
- }
581
- function gridSceneSkew(column, row) {
582
- return column === 3 ? GRID_SCENE_SKEW_VALUES[row] : [0, 0];
583
- }
584
- function gridSceneOuter(column, row) {
585
- return column === 4 ? GRID_SCENE_OUTER_VALUES[row] : "none";
586
- }
587
- /** `_get_grid_rotation`: identity except the rotation column's yaw-then-pitch basis. */
588
- function gridSceneRotation(column, row) {
589
- if (column !== 5)
590
- return [0, 0, 0];
591
- return [-row * Math.PI * 0.035, row * Math.PI * 0.07, 0];
592
- }
593
- /** `_get_grid_bounds` in the XY/Z plane, returned as min/max so callers derive size and x bounds. */
594
- function gridSceneBounds(column, row) {
595
- const [cx, cz] = gridSceneCellCount(column, row);
596
- const [sx, sz] = gridSceneSpacing(column, row);
597
- const [skewX, skewZ] = gridSceneSkew(column, row);
598
- const dx = [sx, 0, sx * Math.tan(skewZ)];
599
- const dz = [sz * Math.tan(skewX), 0, sz];
600
- const start = [-(dx[0] * cx + dz[0] * cz) * 0.5, 0, -(dx[2] * cx + dz[2] * cz) * 0.5];
601
- const corners = [
602
- start,
603
- [start[0] + dx[0] * cx, start[1], start[2] + dx[2] * cx],
604
- [start[0] + dz[0] * cz, start[1], start[2] + dz[2] * cz],
605
- [start[0] + dx[0] * cx + dz[0] * cz, start[1], start[2] + dx[2] * cx + dz[2] * cz],
606
- ];
607
- const rotation = gridSceneRotation(column, row);
608
- let minX = Infinity;
609
- let maxX = -Infinity;
610
- let minZ = Infinity;
611
- let maxZ = -Infinity;
612
- for (const corner of corners) {
613
- const p = rotatePoint(corner, rotation);
614
- minX = Math.min(minX, p[0]);
615
- maxX = Math.max(maxX, p[0]);
616
- minZ = Math.min(minZ, p[2]);
617
- maxZ = Math.max(maxZ, p[2]);
618
- }
619
- return { minX, maxX, minZ, maxZ };
620
- }
621
- function gridSceneColumnWidth(column) {
622
- let width = 0;
623
- for (let row = 0; row < gridSceneRowCount(column); row++) {
624
- const bounds = gridSceneBounds(column, row);
625
- width = Math.max(width, bounds.maxX - bounds.minX);
626
- }
627
- return width;
628
- }
629
- function gridSceneColumnX(column) {
630
- let x = gridSceneColumnWidth(0) * 0.5 + 0.5;
631
- for (let index = 0; index < column; index++) {
632
- x += gridSceneColumnWidth(index) * 0.5 + GRID_SCENE_COLUMN_GAP + gridSceneColumnWidth(index + 1) * 0.5;
633
- }
634
- return x;
635
- }
636
- function gridSceneRowZ(column, row) {
637
- let z = GRID_SCENE_START_Z;
638
- for (let index = 0; index < row; index++) {
639
- const bounds = gridSceneBounds(column, index);
640
- z -= bounds.maxZ - bounds.minZ + GRID_SCENE_SAMPLE_GAP;
641
- }
642
- return z;
643
- }
644
- /** `_get_column_sample_count`: the per-column row table. */
645
- function gridSceneRowCount(column) {
646
- if (column === 0)
647
- return GRID_SCENE_WIDTH_VALUES.length;
648
- if (column === 1)
649
- return GRID_SCENE_CELL_VALUES.length;
650
- if (column === 2)
651
- return GRID_SCENE_SPACING_VALUES.length;
652
- if (column === 3)
653
- return GRID_SCENE_SKEW_VALUES.length;
654
- if (column === 4)
655
- return GRID_SCENE_OUTER_VALUES.length;
656
- if (column === 5)
657
- return GRID_SCENE_ROTATION_COUNT;
658
- if (column === 6)
659
- return GRID_SCENE_COLOR_COUNT;
660
- if (column === 7)
661
- return GRID_SCENE_NORMAL_COUNT;
662
- return 0;
663
- }
664
- function gridSceneCenter(column, row) {
665
- return referenceOffset([gridSceneColumnX(column), GRID_SCENE_Y, gridSceneRowZ(column, row)]);
666
- }
667
- /** `_get_color_sample_color`. */
668
- function gridSceneColorSample(row) {
669
- if (row === 1)
670
- return [1, 0.85, 0.1, 0.35];
671
- if (row === 2)
672
- return [1, 0.45, 0.2, 1];
673
- if (row === 3)
674
- return [0.25, 0.6, 1, 1];
675
- return WHITE;
676
- }
677
- function gridSceneDraw(recorder, column, row, color, width, normal) {
678
- recorder.grid(gridSceneCenter(column, row), gridSceneCellCount(column, row), gridSceneSpacing(column, row), {
679
- color,
680
- width,
681
- rotation: gridSceneRotation(column, row),
682
- skew: gridSceneSkew(column, row),
683
- outer: gridSceneOuter(column, row),
684
- ...(normal === undefined ? {} : { normal }),
685
- });
686
- }
687
- function gridMatrix(recorder) {
688
- drawAxes(recorder, referenceOffset([0, 0, 0]), IDENTITY_BASIS, 0.75, 2);
689
- // grid(width)
690
- GRID_SCENE_WIDTH_VALUES.forEach((width, row) => gridSceneDraw(recorder, 0, row, WHITE, width));
691
- // grid(cells)
692
- GRID_SCENE_CELL_VALUES.forEach((_cells, row) => gridSceneDraw(recorder, 1, row, [0.15, 0.85, 1, 0.95], GRID_SCENE_BASE_LINE_WIDTH));
693
- // grid(spacing)
694
- GRID_SCENE_SPACING_VALUES.forEach((_spacing, row) => gridSceneDraw(recorder, 2, row, [0.7, 1, 0.25, 1], GRID_SCENE_BASE_LINE_WIDTH));
695
- // grid(skew)
696
- GRID_SCENE_SKEW_VALUES.forEach((_skew, row) => gridSceneDraw(recorder, 3, row, [1, 0.45, 0.2, 1], GRID_SCENE_BASE_LINE_WIDTH));
697
- // grid(outer)
698
- GRID_SCENE_OUTER_VALUES.forEach((_outer, row) => gridSceneDraw(recorder, 4, row, [1, 0.7, 0.2, 1], GRID_SCENE_BASE_LINE_WIDTH));
699
- // grid(rotation)
700
- for (let row = 0; row < GRID_SCENE_ROTATION_COUNT; row++)
701
- gridSceneDraw(recorder, 5, row, [0.75, 0.45, 1, 1 - row * 0.08], GRID_SCENE_BASE_LINE_WIDTH);
702
- // grid(color)
703
- for (let row = 0; row < GRID_SCENE_COLOR_COUNT; row++)
704
- gridSceneDraw(recorder, 6, row, gridSceneColorSample(row), GRID_SCENE_BASE_LINE_WIDTH);
705
- // grid_n
706
- for (let row = 0; row < GRID_SCENE_NORMAL_COUNT; row++)
707
- gridSceneDraw(recorder, 7, row, normalSampleColor(row), normalSampleWidth(row, GRID_SCENE_BASE_LINE_WIDTH), [0, 1, 0]);
708
- }
709
- function translateMesh(mesh, position) {
710
- const move = (p) => [p[0] + position[0], p[1] + position[1], p[2] + position[2]];
711
- return { triangles: mesh.triangles.map(([a, b, c]) => [move(a), move(b), move(c)]), segments: mesh.segments.map(([a, b]) => [move(a), move(b)]) };
712
- }
713
- // ---------------------------------------------------------------------------
714
- // Faithful Curve reference port (`examples/curve.gd`).
715
- //
716
- // The 3D curves are CubicBezier `Curve3D`s. `FrameRecorder.curve` only accepts a
717
- // center + euler rotation, so the reference `Basis` transforms are baked into each
718
- // curve's control points before recording.
719
- // ---------------------------------------------------------------------------
720
- const CURVE_LAYOUT = createLayout(SCENE_LAYOUTS.curve, SCENE_LAYOUTS.curve.baseY);
721
- const CURVE_Y = SCENE_LAYOUTS.curve.baseY;
722
- const CURVE_BASE_LINE_WIDTH = 2;
723
- const CURVE_EMPHASIS_LINE_WIDTH = 8;
724
- const CURVE_TOLERANCE_LENGTH = 0.08;
725
- const CURVE_JOINT_TOLERANCE_LENGTH = 0.35;
726
- const CONSTRUCTION_SAMPLE_Z = -0.45;
727
- const CURVE_TUNNEL_DEPTH_STEP = 0.16;
728
- const CURVE_TUNNEL_SCALE_STEP = 0.06;
729
- const CURVE_TUNNEL_TWIST_STEP = Math.PI * 0.12;
730
- const CURVE_SHAPE_VALUES = [0.25, 0.65, 1, 1.35];
731
- const CURVE_TOLERANCE_VALUES = [0.04, 0.08, 0.16, 0.32];
732
- const CURVE_CONSTRUCTION_COLOR = [0.86, 0.94, 1, 1];
733
- // `_draw_curve_control_points` marker constants from `curve.gd`.
734
- const CURVE_ANCHOR_POINT_RADIUS = 0.045;
735
- const CURVE_CONTROL_POINT_RADIUS = 0.032;
736
- const CURVE_CONTROL_POINT_EDGE_WIDTH = 1;
737
- const CURVE_CONTROL_POINT_SEGMENTS = 16;
738
- const CURVE_ANCHOR_POINT_COLOR = [1, 0.25, 0.85, 1];
739
- const CURVE_CONTROL_POINT_COLOR = [1, 0.75, 0.15, 1];
740
- const CURVE_CONTROL_EDGE_COLOR = [1, 0.75, 0.15, 0.85];
741
- /**
742
- * `_get_curve_marker_rotation` for the identity construction transform: it swaps the Y and Z
743
- * basis columns (`Basis(x, z, y)`), which turns the circle's local XY plane into the world XZ
744
- * plane, so every marker is a horizontal ring around a point of the flat construction curve.
745
- */
746
- const CURVE_MARKER_BASIS = { x: [1, 0, 0], y: [0, 0, 1], z: [0, 1, 0] };
747
- // `_create_*_curve` constructors, ported point-for-point (handles are relative offsets).
748
- function straightCurve() {
749
- return curve([
750
- { position: [-0.5, 0, 0.18] },
751
- { position: [0, 0, -0.18] },
752
- { position: [0.5, 0, 0.18] },
753
- ]);
754
- }
755
- function smoothSymmetricCurve(shapeScale = 1) {
756
- return curve([
757
- { position: [-0.55, 0, 0.2 * shapeScale], out: [0.28, 0, -0.35 * shapeScale] },
758
- { position: [0, 0, -0.2 * shapeScale], in: [-0.28, 0, 0.35 * shapeScale], out: [0.28, 0, -0.35 * shapeScale] },
759
- { position: [0.55, 0, 0.2 * shapeScale], in: [-0.28, 0, 0.35 * shapeScale] },
760
- ]);
761
- }
762
- function flatHandleCurve() {
763
- return curve([
764
- { position: [-0.55, 0, 0], out: [0.32, 0, 0] },
765
- { position: [0.55, 0, 0], in: [-0.32, 0, 0] },
766
- ]);
767
- }
768
- function brokenCornerCurve() {
769
- return curve([
770
- { position: [-0.55, 0, 0.18], out: [0.28, 0, -0.08] },
771
- { position: [0, 0, -0.24], in: [-0.18, 0, 0.34], out: [0.12, 0, 0.34] },
772
- { position: [0.55, 0, 0.18], in: [-0.28, 0, -0.08] },
773
- ]);
774
- }
775
- function oneSidedCurve() {
776
- return curve([
777
- { position: [-0.55, 0, 0.18], out: [0.38, 0, -0.48] },
778
- { position: [0, 0, -0.16] },
779
- { position: [0.55, 0, 0.18], in: [-0.38, 0, -0.48] },
780
- ]);
781
- }
782
- function yCurve() {
783
- return curve([
784
- { position: [-0.55, -0.18, 0.18], out: [0.28, 0.36, -0.34] },
785
- { position: [0, 0.22, -0.18], in: [-0.28, -0.34, 0.34], out: [0.28, -0.34, -0.34] },
786
- { position: [0.55, -0.18, 0.18], in: [-0.28, 0.36, 0.34] },
787
- ]);
788
- }
789
- function cornerCurve() {
790
- return curve([
791
- { position: [-0.55, 0, 0.24] },
792
- { position: [-0.15, 0, -0.24] },
793
- { position: [0.18, 0, 0.24] },
794
- { position: [0.55, 0, -0.24] },
795
- ]);
796
- }
797
- function closedCurve(shapeScale) {
798
- const xRadius = 0.45 * shapeScale;
799
- const zRadius = 0.35 * shapeScale;
800
- const xHandle = 0.28 * shapeScale;
801
- const zHandle = 0.24 * shapeScale;
802
- return curve([
803
- { position: [-xRadius, 0, 0], in: [0, 0, zHandle], out: [0, 0, -zHandle] },
804
- { position: [0, 0, -zRadius], in: [-xHandle, 0, 0], out: [xHandle, 0, 0] },
805
- { position: [xRadius, 0, 0], in: [0, 0, -zHandle], out: [0, 0, zHandle] },
806
- { position: [0, 0, zRadius], in: [xHandle, 0, 0], out: [-xHandle, 0, 0] },
807
- ], true);
808
- }
809
- const CURVE_BASE = smoothSymmetricCurve();
810
- const CURVE_CORNER = cornerCurve();
811
- const CURVE_SHAPES = CURVE_SHAPE_VALUES.map((shapeScale) => smoothSymmetricCurve(shapeScale));
812
- const CURVE_CLOSED = [smoothSymmetricCurve(), closedCurve(1), closedCurve(0.75), closedCurve(1.25)];
813
- const CURVE_CONSTRUCTION = [straightCurve(), smoothSymmetricCurve(), flatHandleCurve(), brokenCornerCurve(), oneSidedCurve(), yCurve(), closedCurve(1)];
814
- /** reference `_get_curve_rotation`: `Basis(width_axis, normal, depth_axis)`. */
815
- function curveRotationBasis(normal, angle) {
816
- const n = normalizeVector(normal);
817
- const reference = Math.abs(n[0]) > 0.95 ? [0, 0, 1] : [1, 0, 0];
818
- const projection = reference[0] * n[0] + reference[1] * n[1] + reference[2] * n[2];
819
- const planar = [reference[0] - n[0] * projection, reference[1] - n[1] * projection, reference[2] - n[2] * projection];
820
- const widthAxis = normalizeVector(rotateAroundAxis(normalizeVector(planar), n, angle));
821
- const depthAxis = normalizeVector(crossVector(widthAxis, n));
822
- return { x: widthAxis, y: n, z: depthAxis };
823
- }
824
- /** `_get_rotation_tunnel_rotation(row)`. */
825
- function curveTunnelBasis(row) {
826
- const tilted = rotateAroundAxis([0, 1, 0], [1, 0, 0], -Math.PI * 0.38);
827
- const normal = normalizeVector(rotateAroundAxis(tilted, [0, 0, 1], row * CURVE_TUNNEL_TWIST_STEP));
828
- return curveRotationBasis(normal, row * Math.PI * 0.08);
829
- }
830
- /** `_get_rotation_tunnel_center`: lift the row, then recess it along -Z. */
831
- function curveTunnelCenter(column, row) {
832
- const center = CURVE_LAYOUT.atLocal(column, row);
833
- return referenceOffset([center[0], center[1] + 0.35, center[2] - row * CURVE_TUNNEL_DEPTH_STEP]);
834
- }
835
- /** Bakes a uniform scale + `Basis` into control points; only the translation is passed to `record`. */
836
- function scaleCurve(shape, scale, basis) {
837
- const map = (p) => [
838
- (basis.x[0] * p[0] + basis.y[0] * p[1] + basis.z[0] * p[2]) * scale,
839
- (basis.x[1] * p[0] + basis.y[1] * p[1] + basis.z[1] * p[2]) * scale,
840
- (basis.x[2] * p[0] + basis.y[2] * p[1] + basis.z[2] * p[2]) * scale,
841
- ];
842
- const points = shape.points.map((point) => ({
843
- position: map(point.position),
844
- ...(point.in === undefined ? {} : { in: map(point.in) }),
845
- ...(point.out === undefined ? {} : { out: map(point.out) }),
846
- }));
847
- return { points, ...(shape.closed === undefined ? {} : { closed: shape.closed }) };
848
- }
849
- function placeCurve(recorder, shape, center, options, scale = 1, basis = IDENTITY_BASIS) {
850
- recorder.curve(scaleCurve(shape, scale, basis), { ...options, transform: { center } });
851
- }
852
- /** reference `Vector3.is_zero_approx`: every component under `CMP_EPSILON`. */
853
- function isZeroApprox(value) {
854
- return Math.abs(value[0]) < 1e-5 && Math.abs(value[1]) < 1e-5 && Math.abs(value[2]) < 1e-5;
855
- }
856
- function addVec3(a, b) {
857
- return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
858
- }
859
- /**
860
- * Reference `_draw_curve_control_points` / `_draw_curve_control_handle`: for every Bezier
861
- * point of a construction curve, draw the incoming and outgoing handles as yellow edges with a
862
- * yellow ring at the tip, then the pink anchor ring on top. `localCenter` is untranslated
863
- * because `basisCirclePoints` applies the scene translation itself.
864
- */
865
- function drawCurveControlPoints(recorder, shape, localCenter) {
866
- const ring = (local, radius, color) => {
867
- recorder.lineStrip(basisCirclePoints(local, radius, CURVE_CONTROL_POINT_SEGMENTS, CURVE_MARKER_BASIS), { color, width: CURVE_BASE_LINE_WIDTH, joint: "round" }, true);
868
- };
869
- for (const point of shape.points) {
870
- const anchor = addVec3(localCenter, point.position);
871
- for (const handle of [point.in, point.out]) {
872
- if (handle === undefined || isZeroApprox(handle))
873
- continue;
874
- const tip = addVec3(anchor, handle);
875
- recorder.line(referenceOffset(anchor), referenceOffset(tip), { color: CURVE_CONTROL_EDGE_COLOR, width: CURVE_CONTROL_POINT_EDGE_WIDTH });
876
- ring(tip, CURVE_CONTROL_POINT_RADIUS, CURVE_CONTROL_POINT_COLOR);
877
- }
878
- ring(anchor, CURVE_ANCHOR_POINT_RADIUS, CURVE_ANCHOR_POINT_COLOR);
879
- }
880
- }
881
- function curveMatrix(recorder, context) {
882
- const dashOffset = fposmod(context.time * DASH_OFFSET_SPEED, 10);
883
- // Construction band at CONSTRUCTION_SAMPLE_Z, including the reference control-point markers.
884
- CURVE_CONSTRUCTION.forEach((shape, index) => {
885
- const localCenter = [CURVE_LAYOUT.columnX(index), CURVE_Y, CONSTRUCTION_SAMPLE_Z];
886
- const center = referenceOffset(localCenter);
887
- recorder.curve(shape, { color: CURVE_CONSTRUCTION_COLOR, width: CURVE_BASE_LINE_WIDTH, tolerance: CURVE_TOLERANCE_LENGTH, joint: "round", transform: { center } });
888
- drawCurveControlPoints(recorder, shape, localCenter);
889
- });
890
- // curve(width)
891
- WIDTH_VALUES.forEach((width, row) => {
892
- placeCurve(recorder, CURVE_BASE, CURVE_LAYOUT.at(0, row), { color: WHITE, width, tolerance: CURVE_TOLERANCE_LENGTH });
893
- });
894
- // curve(shape)
895
- CURVE_SHAPES.forEach((shape, row) => {
896
- placeCurve(recorder, shape, CURVE_LAYOUT.at(1, row), { color: CYAN, width: CURVE_BASE_LINE_WIDTH, tolerance: CURVE_TOLERANCE_LENGTH });
897
- });
898
- // curve(transform)
899
- for (let row = 0; row < ROTATION_SAMPLE_COUNT; row++) {
900
- const scale = 1 - row * CURVE_TUNNEL_SCALE_STEP;
901
- placeCurve(recorder, CURVE_BASE, curveTunnelCenter(2, row), { color: [0.7, 1, 0.25, 1 - row * 0.07], width: CURVE_BASE_LINE_WIDTH, tolerance: CURVE_TOLERANCE_LENGTH }, scale, curveTunnelBasis(row));
902
- }
903
- // curve(tolerance)
904
- CURVE_TOLERANCE_VALUES.forEach((tolerance, row) => {
905
- placeCurve(recorder, CURVE_BASE, CURVE_LAYOUT.at(3, row), { color: GOLD, width: CURVE_BASE_LINE_WIDTH, tolerance });
906
- });
907
- // curve(closed)
908
- CURVE_CLOSED.forEach((shape, row) => {
909
- placeCurve(recorder, shape, CURVE_LAYOUT.at(4, row), { color: TEAL, width: CURVE_BASE_LINE_WIDTH, tolerance: CURVE_TOLERANCE_LENGTH, joint: "round" });
910
- });
911
- // curve(joint)
912
- JOINT_VALUES.forEach((joint, row) => {
913
- placeCurve(recorder, CURVE_CORNER, CURVE_LAYOUT.at(5, row), { color: VIOLET, width: CURVE_EMPHASIS_LINE_WIDTH, tolerance: CURVE_JOINT_TOLERANCE_LENGTH, joint });
914
- });
915
- // curve_d(dash)
916
- DASH_VALUES.forEach(([dash, space], row) => {
917
- placeCurve(recorder, CURVE_BASE, CURVE_LAYOUT.at(6, row), { color: ORANGE_FULL, width: CURVE_BASE_LINE_WIDTH, tolerance: CURVE_TOLERANCE_LENGTH, dash: [dash, space, fposmod(dashOffset, dash + space)] });
918
- });
919
- // curve_d(joint)
920
- JOINT_VALUES.forEach((joint, row) => {
921
- placeCurve(recorder, CURVE_CORNER, CURVE_LAYOUT.at(7, row), { color: PINK, width: CURVE_EMPHASIS_LINE_WIDTH, tolerance: CURVE_JOINT_TOLERANCE_LENGTH, joint, dash: [0.18, 0.1, fposmod(dashOffset, 0.28)] });
922
- });
923
- }
924
- function circleMatrix(recorder, context) {
925
- const dashOffset = context.time * CIRCLE_DASH_SPEED;
926
- const flat = circleBasis([0, 1, 0], 0);
927
- const circle = (column, row, radius, segments, basis = flat) => basisCirclePoints(circleCenterLocal(column, row), radius, segments, basis);
928
- WIDTH_VALUES.forEach((width, row) => recorder.lineStrip(circle(0, row, CIRCLE_RADIUS, CIRCLE_SEGMENTS), { color: WHITE, width }, true));
929
- RADIUS_VALUES.forEach((radius, row) => recorder.lineStrip(circle(1, row, radius, CIRCLE_SEGMENTS), { color: CYAN, width: 2 }, true));
930
- SEGMENT_VALUES.forEach((segments, row) => recorder.lineStrip(circle(2, row, CIRCLE_RADIUS, segments), { color: LIME, width: 2 }, true));
931
- for (let row = 0; row < CIRCLE_ROTATION_SAMPLES; row++) {
932
- const tilted = rotateAroundAxis([0, 1, 0], [1, 0, 0], -Math.PI * 0.38);
933
- const normal = normalizeVector(rotateAroundAxis(tilted, [0, 0, 1], row * CIRCLE_TUNNEL_TWIST_STEP));
934
- const basis = circleBasis(normal, row * Math.PI * 0.08);
935
- const center = [circleColumnX(3), CIRCLE_Y + 0.35, CIRCLE_SAMPLE_START_Z - row * CIRCLE_ROW_SPACING - row * CIRCLE_TUNNEL_DEPTH_STEP];
936
- const scale = 1 - row * CIRCLE_TUNNEL_SCALE_STEP;
937
- recorder.lineStrip(basisCirclePoints(center, CIRCLE_TUNNEL_RADIUS * scale, CIRCLE_SEGMENTS, basis), { color: [0.95, 0.75, 0.25, 1 - row * 0.07], width: 2 }, true);
938
- }
939
- JOINT_VALUES.forEach((joint, row) => recorder.lineStrip(circle(4, row, CIRCLE_RADIUS, CIRCLE_JOINT_SEGMENTS), { color: VIOLET, width: 8, joint }, true));
940
- DASH_VALUES.forEach(([dash, space], row) => recorder.lineStrip(circle(5, row, CIRCLE_RADIUS, CIRCLE_SEGMENTS), { color: ORANGE, width: 2, dash: [dash, space, dashOffset] }, true));
941
- JOINT_VALUES.forEach((joint, row) => recorder.lineStrip(circle(6, row, CIRCLE_RADIUS, CIRCLE_JOINT_SEGMENTS), { color: [1, 0.3, 0.6, 1], width: 8, joint, dash: [0.18, 0.1, dashOffset] }, true));
942
- for (let row = 0; row < 4; row++) {
943
- const color = row === 1 ? [0.25, 0.95, 0.9, 0.35] : row === 2 ? [1, 0.45, 0.2, 1] : TEAL;
944
- const width = row === 3 ? 10 : CIRCLE_BASE_WIDTH;
945
- drawCircleNormals(recorder, circle(7, row, CIRCLE_RADIUS, CIRCLE_SEGMENTS), CIRCLE_SEGMENTS, flat, { color, width, joint: "round" });
946
- }
947
- DASH_VALUES.forEach(([dash, space], row) => drawCircleNormals(recorder, circle(8, row, CIRCLE_RADIUS, CIRCLE_SEGMENTS), CIRCLE_SEGMENTS, flat, { color: TEAL, width: CIRCLE_BASE_WIDTH, dash: [dash, space, dashOffset], joint: "round" }));
948
- JOINT_VALUES.forEach((joint, row) => drawCircleNormals(recorder, circle(9, row, CIRCLE_RADIUS, CIRCLE_JOINT_SEGMENTS), CIRCLE_JOINT_SEGMENTS, flat, { color: TEAL, width: 8, joint }));
949
- SEGMENT_VALUES.forEach((segments, row) => {
950
- const center = referenceOffset(circleCenterLocal(10, row));
951
- recorder.mesh(translateMesh(disc(CIRCLE_RADIUS, segments), center), { color: [0.25, 0.65, 1, 0.45], solid: true });
952
- });
953
- }
954
- // ---------------------------------------------------------------------------
955
- // Faithful Arc reference port (`examples/arc.gd`).
956
- // ---------------------------------------------------------------------------
957
- const ARC_LAYOUT = createLayout(SCENE_LAYOUTS.arc, SCENE_LAYOUTS.arc.baseY);
958
- const ARC_RADIUS = 0.34;
959
- const ARC_ANGLE = Math.PI * 1.5;
960
- const ARC_BASE_LINE_WIDTH = 2;
961
- const ARC_EMPHASIS_LINE_WIDTH = 8;
962
- const ARC_SEGMENT_COUNT = 64;
963
- const ARC_JOINT_SEGMENT_COUNT = 12;
964
- const ARC_ANGLE_VALUES = [Math.PI * 0.5, Math.PI, Math.PI * 1.5, Math.PI * 2];
965
- const ARC_TUNNEL_RADIUS = 0.44;
966
- const ARC_TUNNEL_DEPTH_STEP = 0.16;
967
- const ARC_TUNNEL_SCALE_STEP = 0.06;
968
- const ARC_TUNNEL_TWIST_STEP = Math.PI * 0.12;
969
- const FLAT_BASIS = circleBasis([0, 1, 0], 0);
970
- /** `_get_rotation_tunnel_rotation(row)`. */
971
- function arcTunnelBasis(row) {
972
- const tilted = rotateAroundAxis([0, 1, 0], [1, 0, 0], -Math.PI * 0.38);
973
- const normal = normalizeVector(rotateAroundAxis(tilted, [0, 0, 1], row * ARC_TUNNEL_TWIST_STEP));
974
- return circleBasis(normal, row * Math.PI * 0.08);
975
- }
976
- /** `_get_rotation_tunnel_center`: lift the row, then recess it along -Z. */
977
- function arcTunnelCenter(column, row) {
978
- const center = ARC_LAYOUT.atLocal(column, row);
979
- return referenceOffset([center[0], center[1] + 0.35, center[2] - row * ARC_TUNNEL_DEPTH_STEP]);
980
- }
981
- function arcMatrix(recorder, context) {
982
- const dashOffset = fposmod(context.time * DASH_OFFSET_SPEED, 10);
983
- // arc(width)
984
- WIDTH_VALUES.forEach((width, row) => {
985
- recorder.lineStrip(arcPoints(ARC_LAYOUT.at(0, row), ARC_RADIUS, ARC_ANGLE, ARC_SEGMENT_COUNT, FLAT_BASIS), { color: WHITE, width }, false);
986
- });
987
- // arc(radius)
988
- RADIUS_VALUES.forEach((radius, row) => {
989
- recorder.lineStrip(arcPoints(ARC_LAYOUT.at(1, row), radius, ARC_ANGLE, ARC_SEGMENT_COUNT, FLAT_BASIS), { color: CYAN, width: ARC_BASE_LINE_WIDTH }, false);
990
- });
991
- // arc(angle)
992
- ARC_ANGLE_VALUES.forEach((angle, row) => {
993
- recorder.lineStrip(arcPoints(ARC_LAYOUT.at(2, row), ARC_RADIUS, angle, ARC_SEGMENT_COUNT, FLAT_BASIS), { color: LIME, width: ARC_BASE_LINE_WIDTH }, false);
994
- });
995
- // arc(segments)
996
- SEGMENT_VALUES.forEach((segments, row) => {
997
- recorder.lineStrip(arcPoints(ARC_LAYOUT.at(3, row), ARC_RADIUS, ARC_ANGLE, segments, FLAT_BASIS), { color: GOLD, width: ARC_BASE_LINE_WIDTH }, false);
998
- });
999
- // arc(rotation)
1000
- for (let row = 0; row < ROTATION_SAMPLE_COUNT; row++) {
1001
- const scale = 1 - row * ARC_TUNNEL_SCALE_STEP;
1002
- recorder.lineStrip(arcPoints(arcTunnelCenter(4, row), ARC_TUNNEL_RADIUS * scale, ARC_ANGLE, ARC_SEGMENT_COUNT, arcTunnelBasis(row)), { color: [0.95, 0.75, 0.25, 1 - row * 0.07], width: ARC_BASE_LINE_WIDTH }, false);
1003
- }
1004
- // arc(joint)
1005
- JOINT_VALUES.forEach((joint, row) => {
1006
- recorder.lineStrip(arcPoints(ARC_LAYOUT.at(5, row), ARC_RADIUS, ARC_ANGLE, ARC_JOINT_SEGMENT_COUNT, FLAT_BASIS), { color: VIOLET, width: ARC_EMPHASIS_LINE_WIDTH, joint }, false);
1007
- });
1008
- // arc_d(dash)
1009
- DASH_VALUES.forEach(([dash, space], row) => {
1010
- recorder.lineStrip(arcPoints(ARC_LAYOUT.at(6, row), ARC_RADIUS, ARC_ANGLE, ARC_SEGMENT_COUNT, FLAT_BASIS), { color: ORANGE_FULL, width: ARC_BASE_LINE_WIDTH, dash: [dash, space, fposmod(dashOffset, dash + space)] }, false);
1011
- });
1012
- // arc_d(joint)
1013
- JOINT_VALUES.forEach((joint, row) => {
1014
- recorder.lineStrip(arcPoints(ARC_LAYOUT.at(7, row), ARC_RADIUS, ARC_ANGLE, ARC_JOINT_SEGMENT_COUNT, FLAT_BASIS), { color: PINK, width: ARC_EMPHASIS_LINE_WIDTH, joint, dash: [0.18, 0.1, fposmod(dashOffset, 0.28)] }, false);
1015
- });
1016
- // arc_n
1017
- for (let row = 0; row < NORMAL_SAMPLE_COUNT; row++) {
1018
- drawArcNormals(recorder, arcPoints(ARC_LAYOUT.at(8, row), ARC_RADIUS, ARC_ANGLE, ARC_SEGMENT_COUNT, FLAT_BASIS), ARC_ANGLE, ARC_SEGMENT_COUNT, FLAT_BASIS, { color: normalSampleColor(row), width: normalSampleWidth(row, ARC_BASE_LINE_WIDTH), joint: "round" });
1019
- }
1020
- // arc_n_d(dash)
1021
- DASH_VALUES.forEach(([dash, space], row) => {
1022
- drawArcNormals(recorder, arcPoints(ARC_LAYOUT.at(9, row), ARC_RADIUS, ARC_ANGLE, ARC_SEGMENT_COUNT, FLAT_BASIS), ARC_ANGLE, ARC_SEGMENT_COUNT, FLAT_BASIS, { color: TEAL, width: ARC_BASE_LINE_WIDTH, dash: [dash, space, fposmod(dashOffset, dash + space)], joint: "round" });
1023
- });
1024
- // arc_n(joint)
1025
- JOINT_VALUES.forEach((joint, row) => {
1026
- drawArcNormals(recorder, arcPoints(ARC_LAYOUT.at(10, row), ARC_RADIUS, ARC_ANGLE, ARC_JOINT_SEGMENT_COUNT, FLAT_BASIS), ARC_ANGLE, ARC_JOINT_SEGMENT_COUNT, FLAT_BASIS, { color: TEAL, width: ARC_EMPHASIS_LINE_WIDTH, joint });
1027
- });
1028
- }
1029
- function rectMatrix(recorder, context) {
1030
- const dashOffset = context.time * DASH_OFFSET_SPEED;
1031
- // rect(width)
1032
- WIDTH_VALUES.forEach((width, row) => {
1033
- recorder.lineStrip(rectLoop(RECT_LAYOUT.at(0, row), RECT_FLAT_BASIS, RECT_WIDTH, RECT_HEIGHT), { color: WHITE, width }, true);
1034
- });
1035
- // rect(size)
1036
- RECT_SIZE_VALUES.forEach(([width, height], row) => {
1037
- recorder.lineStrip(rectLoop(RECT_LAYOUT.at(1, row), RECT_FLAT_BASIS, width, height), { color: CYAN, width: RECT_BASE_LINE_WIDTH }, true);
1038
- });
1039
- // rect(rotation)
1040
- for (let row = 0; row < ROTATION_SAMPLE_COUNT; row++) {
1041
- const scale = 1 - row * RECT_ROTATION_TUNNEL_SCALE_STEP;
1042
- const center = rectTunnelCenter(2, row, 0.35, RECT_ROTATION_TUNNEL_DEPTH_STEP);
1043
- const basis = rectRotationTunnelBasis(row);
1044
- const color = [0.7, 1, 0.25, 1 - row * 0.07];
1045
- recorder.lineStrip(rectLoop(center, basis, RECT_ROTATION_TUNNEL_WIDTH * scale, RECT_ROTATION_TUNNEL_HEIGHT * scale), { color, width: RECT_BASE_LINE_WIDTH }, true);
1046
- }
1047
- // rect(joint)
1048
- JOINT_VALUES.forEach((joint, row) => {
1049
- recorder.lineStrip(rectLoop(RECT_LAYOUT.at(3, row), RECT_FLAT_BASIS, RECT_WIDTH, RECT_HEIGHT), { color: VIOLET, width: 8, joint }, true);
1050
- });
1051
- // rect_d(dash)
1052
- DASH_VALUES.forEach(([dash, space], row) => {
1053
- recorder.lineStrip(rectLoop(RECT_LAYOUT.at(4, row), RECT_FLAT_BASIS, RECT_WIDTH, RECT_HEIGHT), { color: ORANGE_FULL, width: RECT_BASE_LINE_WIDTH, dash: [dash, space, dashOffset] }, true);
1054
- });
1055
- // rect_d(joint)
1056
- JOINT_VALUES.forEach((joint, row) => {
1057
- recorder.lineStrip(rectLoop(RECT_LAYOUT.at(5, row), RECT_FLAT_BASIS, RECT_WIDTH, RECT_HEIGHT), { color: PINK, width: 8, dash: [0.18, 0.1, dashOffset], joint }, true);
1058
- });
1059
- // rect_n
1060
- for (let row = 0; row < NORMAL_SAMPLE_COUNT; row++) {
1061
- drawRectNormals(recorder, rectLoop(RECT_LAYOUT.at(6, row), RECT_FLAT_BASIS, RECT_WIDTH, RECT_HEIGHT), RECT_FLAT_BASIS, { color: normalSampleColor(row), width: normalSampleWidth(row, RECT_BASE_LINE_WIDTH), joint: "round" });
1062
- }
1063
- // rect_n_d(dash)
1064
- DASH_VALUES.forEach(([dash, space], row) => {
1065
- drawRectNormals(recorder, rectLoop(RECT_LAYOUT.at(7, row), RECT_FLAT_BASIS, RECT_WIDTH, RECT_HEIGHT), RECT_FLAT_BASIS, { color: TEAL, width: RECT_BASE_LINE_WIDTH, dash: [dash, space, dashOffset], joint: "round" });
1066
- });
1067
- // rect_n(joint)
1068
- JOINT_VALUES.forEach((joint, row) => {
1069
- drawRectNormals(recorder, rectLoop(RECT_LAYOUT.at(8, row), RECT_FLAT_BASIS, RECT_WIDTH, RECT_HEIGHT), RECT_FLAT_BASIS, { color: TEAL, width: RECT_EMPHASIS_LINE_WIDTH, joint });
1070
- });
1071
- // rect_s(solid)
1072
- for (let row = 0; row < RECT_S_SAMPLE_COUNT; row++) {
1073
- const scale = 1 - row * RECT_S_TUNNEL_SCALE_STEP;
1074
- const hue = row / RECT_S_SAMPLE_COUNT + 0.6;
1075
- const center = rectTunnelCenter(9, row, 0.5, RECT_S_TUNNEL_DEPTH_STEP);
1076
- const basis = rectSolidBasis(row);
1077
- const color = fromHsv(hue, 0.7, 1, 1 - row * 0.08);
1078
- recorder.mesh(rectMesh(center, basis, RECT_S_TUNNEL_WIDTH * scale, RECT_S_TUNNEL_HEIGHT * scale), { color, solid: true });
1079
- }
1080
- }
1081
- function boxMatrix(recorder, context) {
1082
- const dashOffset = context.time * DASH_OFFSET_SPEED;
1083
- // box(width)
1084
- WIDTH_VALUES.forEach((width, row) => {
1085
- drawBox(recorder, boxCenter(0, row), IDENTITY_BASIS, BOX_SIZE, { color: WHITE, width });
1086
- });
1087
- // box(size)
1088
- BOX_SIZE_VALUES.forEach((size, row) => {
1089
- drawBox(recorder, boxCenter(1, row), IDENTITY_BASIS, size, { color: CYAN, width: BOX_BASE_LINE_WIDTH });
1090
- });
1091
- // box(rotation)
1092
- for (let row = 0; row < ROTATION_SAMPLE_COUNT; row++) {
1093
- const scale = 1 - row * BOX_ROTATION_TUNNEL_SCALE_STEP;
1094
- const center = boxTunnelCenter(2, row, BOX_ROTATION_TUNNEL_Y, BOX_ROTATION_TUNNEL_DEPTH_STEP);
1095
- const color = [0.7, 1, 0.25, 1 - row * 0.07];
1096
- drawBox(recorder, center, boxRotationTunnelBasis(row), scaleVec(BOX_ROTATION_TUNNEL_SIZE, scale), { color, width: BOX_BASE_LINE_WIDTH });
1097
- }
1098
- // box_d(dash)
1099
- DASH_VALUES.forEach(([dash, space], row) => {
1100
- drawBox(recorder, boxCenter(3, row), IDENTITY_BASIS, BOX_SIZE, { color: ORANGE_FULL, width: BOX_BASE_LINE_WIDTH, dash: [dash, space, fposmod(dashOffset, dash + space)] });
1101
- });
1102
- // box_n
1103
- for (let row = 0; row < NORMAL_SAMPLE_COUNT; row++) {
1104
- drawBoxNormals(recorder, boxCenter(4, row), IDENTITY_BASIS, BOX_SIZE, { color: normalSampleColor(row), width: normalSampleWidth(row, BOX_BASE_LINE_WIDTH) });
1105
- }
1106
- // box_n_d(dash)
1107
- DASH_VALUES.forEach(([dash, space], row) => {
1108
- drawBoxNormals(recorder, boxCenter(5, row), IDENTITY_BASIS, BOX_SIZE, { color: TEAL, width: BOX_BASE_LINE_WIDTH, dash: [dash, space, fposmod(dashOffset, dash + space)] });
1109
- });
1110
- // box_s(solid): the reference also overlays the normal-aware wireframe.
1111
- for (let row = 0; row < BOX_S_SAMPLE_COUNT; row++) {
1112
- const scale = 1 - row * BOX_S_TUNNEL_SCALE_STEP;
1113
- const hue = row / BOX_S_SAMPLE_COUNT + 0.6;
1114
- const center = boxTunnelCenter(6, row, BOX_S_TUNNEL_Y, BOX_S_TUNNEL_DEPTH_STEP);
1115
- const basis = boxSolidBasis(row);
1116
- const size = scaleVec(BOX_S_TUNNEL_SIZE, scale);
1117
- recorder.mesh(transformMeshByBasis(box(size), center, basis), { color: fromHsv(hue, 0.7, 1, 0.35), solid: true });
1118
- drawBox(recorder, center, basis, size, { color: fromHsv(hue, 0.7, 1, 1), width: BOX_BASE_LINE_WIDTH });
1119
- }
1120
- }
1121
- // ---------------------------------------------------------------------------
1122
- // Faithful Index / World Space Width reference ports (`examples/index.gd`,
1123
- // `examples/world_space_width.gd`).
1124
- //
1125
- // Both scenes drive the same 17-column API matrix. `world_space_width` keeps the
1126
- // layout but converts every line width from screen pixels to world units
1127
- // (`_world_width(px) = px * 0.01`) and drops the `*_s` solid columns, so the two
1128
- // share one implementation that differs only in width conversion, width mode and
1129
- // whether solid columns are included.
1130
- //
1131
- // Approximations (also noted in `the parity notes`):
1132
- // - `Label3D` column and sample labels are omitted; the gallery draws its own.
1133
- // - `draw_curve` uses a fixed `tolerance` instead of adaptive `max_stages` sampling.
1134
- // - Arrow heads keep the flat X-strip approximation shared with `arrow.gd`.
1135
- // - `draw_grid`/`draw_grid_n` use the library grid, whose skew shear and outer
1136
- // edge semantics are the port's own (see the grid scene note).
1137
- // - Normal-aware columns pass one representative normal; cone/cylinder/capsule
1138
- // use one segment count for the base ring and the side lines.
1139
- // ---------------------------------------------------------------------------
1140
- const INDEX_LAYOUT = createLayout(SCENE_LAYOUTS.index, SCENE_LAYOUTS.index.baseY);
1141
- const INDEX_DASH_WIDTH = 0.16;
1142
- const INDEX_DASH_SPACE = 0.09;
1143
- const INDEX_DASH_OFFSET = 0;
1144
- const WSW_LINE_WIDTH_SCALE = 0.01;
1145
- const INDEX_UP = [0, 1, 0];
1146
- const INDEX_BRIGHT_CYAN = [0.35, 1, 1, 1];
1147
- const INDEX_GOLD = [1, 0.75, 0.25, 1];
1148
- const INDEX_MINT = [0.5, 1, 0.65, 1];
1149
- const INDEX_MAGENTA = [1, 0.45, 1, 1];
1150
- const INDEX_LAVENDER = [0.7, 0.7, 1, 1];
1151
- /** `index.gd` `_p(column, row, x_offset, z_offset, y)` with the capture translation applied. */
1152
- function indexPath(column, row, xOffset = 0, zOffset = 0, y = SCENE_LAYOUTS.index.baseY) {
1153
- const local = INDEX_LAYOUT.atLocal(column, row);
1154
- return referenceOffset([local[0] + xOffset, y, local[2] + zOffset]);
1155
- }
1156
- /** `index.gd`'s `_curve` control points (three cubic Bezier anchors). */
1157
- const INDEX_CURVE = curve([
1158
- { position: [-0.45, -0.15, 0], in: [0, 0, 0], out: [0.25, 0.4, 0] },
1159
- { position: [0.15, 0.25, 0.25], in: [-0.25, 0.35, 0], out: [0.25, -0.35, 0] },
1160
- { position: [0.65, -0.15, 0.45], in: [-0.25, -0.35, 0], out: [0, 0, 0] },
1161
- ]);
1162
- /** `draw_line_list`: independent segments taken as consecutive point pairs. */
1163
- function drawLineList(recorder, points, options) {
1164
- for (let index = 0; index + 1 < points.length; index += 2)
1165
- recorder.line(points[index], points[index + 1], options);
1166
- }
1167
- function apiMatrix(recorder, config) {
1168
- const { width: w, widthMode, solids } = config;
1169
- const dash = [INDEX_DASH_WIDTH, INDEX_DASH_SPACE, INDEX_DASH_OFFSET];
1170
- drawAxes(recorder, referenceOffset([0, 0, 0]), IDENTITY_BASIS, 0.75, w(2), widthMode);
1171
- // line
1172
- recorder.line(indexPath(0, 0), indexPath(0, 0, 1, 0, 0.9), { color: WHITE, width: w(2), widthMode });
1173
- recorder.line(indexPath(0, 1), indexPath(0, 1, 1, 0, 0.9), { color: INDEX_BRIGHT_CYAN, width: w(2), widthMode, normal: INDEX_UP });
1174
- recorder.line(indexPath(0, 2), indexPath(0, 2, 1, 0, 0.9), { color: INDEX_GOLD, width: w(2), widthMode, dash });
1175
- recorder.line(indexPath(0, 3), indexPath(0, 3, 1, 0, 0.9), { color: INDEX_MINT, width: w(2), widthMode, normal: INDEX_UP, dash });
1176
- // ray
1177
- const ray = (row, options) => {
1178
- const origin = indexPath(1, row);
1179
- recorder.line(origin, [origin[0] + 1, origin[1] + 0.35, origin[2]], options);
1180
- };
1181
- ray(0, { color: WHITE, width: w(2), widthMode });
1182
- ray(1, { color: INDEX_BRIGHT_CYAN, width: w(2), widthMode, normal: INDEX_UP });
1183
- ray(2, { color: INDEX_GOLD, width: w(2), widthMode, dash });
1184
- ray(3, { color: INDEX_MINT, width: w(2), widthMode, normal: INDEX_UP, dash });
1185
- // arrow
1186
- drawArrowHead(recorder, indexPath(2, 0, 1, 0, 0.9), [0.55, 0.2, 0], { color: WHITE, width: w(2), widthMode, headAngle: ARROW_BASE_HEAD_ANGLE, joint: "round" });
1187
- drawArrowHead(recorder, indexPath(2, 1, 1, 0, 0.9), [0.55, 0.2, 0], { color: INDEX_BRIGHT_CYAN, width: w(2), widthMode, headAngle: ARROW_BASE_HEAD_ANGLE, joint: "bevel", normal: INDEX_UP });
1188
- drawArrowLine(recorder, indexPath(2, 2), indexPath(2, 2, 1, 0, 0.9), { color: INDEX_GOLD, width: w(2), widthMode, headLength: 0.25, headAngle: ARROW_BASE_HEAD_ANGLE, joint: "round" });
1189
- drawArrowLine(recorder, indexPath(2, 3), indexPath(2, 3, 1, 0, 0.9), { color: INDEX_MINT, width: w(2), widthMode, headLength: 0.25, headAngle: ARROW_BASE_HEAD_ANGLE, joint: "bevel", normal: INDEX_UP });
1190
- drawArrowLine(recorder, indexPath(2, 4), indexPath(2, 4, 1, 0, 0.9), { color: INDEX_MAGENTA, width: w(2), widthMode, headLength: 0.25, headAngle: ARROW_BASE_HEAD_ANGLE, joint: "miter", dash });
1191
- drawArrowLine(recorder, indexPath(2, 5), indexPath(2, 5, 1, 0, 0.9), { color: INDEX_LAVENDER, width: w(2), widthMode, headLength: 0.25, headAngle: ARROW_BASE_HEAD_ANGLE, joint: "round", normal: INDEX_UP, dash });
1192
- // axes
1193
- drawAxes(recorder, indexPath(3, 0, 0.5), IDENTITY_BASIS, 0.55, w(2), widthMode);
1194
- drawAxesN(recorder, indexPath(3, 1, 0.5), rotateBasis(IDENTITY_BASIS, INDEX_UP, Math.PI * 0.25), 0.55, w(2), widthMode);
1195
- // line_list
1196
- const listPoints = (column, row) => [
1197
- indexPath(column, row),
1198
- indexPath(column, row, 0.8, 0, 0.85),
1199
- indexPath(column, row, 0.1, 0.35),
1200
- indexPath(column, row, 0.9, 0.35, 0.85),
1201
- ];
1202
- drawLineList(recorder, listPoints(4, 0), { color: WHITE, width: w(2), widthMode });
1203
- drawLineList(recorder, listPoints(4, 1), { color: INDEX_BRIGHT_CYAN, width: w(2), widthMode, normal: INDEX_UP });
1204
- // strip
1205
- const stripPoints = (column, row) => [indexPath(column, row), indexPath(column, row, 0.45, 0.18, 0.95), indexPath(column, row, 1, 0, 0.7)];
1206
- recorder.lineStrip(stripPoints(5, 0), { color: WHITE, width: w(4), widthMode, joint: "round" }, false);
1207
- recorder.lineStrip(stripPoints(5, 1), { color: INDEX_BRIGHT_CYAN, width: w(4), widthMode, joint: "bevel", normal: INDEX_UP }, false);
1208
- recorder.lineStrip(stripPoints(5, 2), { color: INDEX_GOLD, width: w(4), widthMode, joint: "miter", dash }, false);
1209
- recorder.lineStrip(stripPoints(5, 3), { color: INDEX_MINT, width: w(4), widthMode, joint: "round", normal: INDEX_UP, dash }, false);
1210
- // loop
1211
- const loopPoints = (column, row) => [indexPath(column, row), indexPath(column, row, 0.55, 0.25, 0.95), indexPath(column, row, 1, 0, 0.65)];
1212
- recorder.lineStrip(loopPoints(6, 0), { color: WHITE, width: w(4), widthMode, joint: "round" }, true);
1213
- recorder.lineStrip(loopPoints(6, 1), { color: INDEX_BRIGHT_CYAN, width: w(4), widthMode, joint: "bevel", normal: INDEX_UP }, true);
1214
- recorder.lineStrip(loopPoints(6, 2), { color: INDEX_GOLD, width: w(4), widthMode, joint: "miter", dash }, true);
1215
- recorder.lineStrip(loopPoints(6, 3), { color: INDEX_MINT, width: w(4), widthMode, joint: "round", normal: INDEX_UP, dash }, true);
1216
- // curve
1217
- recorder.curve(INDEX_CURVE, { color: WHITE, width: w(2.5), widthMode, tolerance: 0.04, joint: "round", transform: { center: indexPath(7, 0, 0.5, 0, 0.65) } });
1218
- recorder.curve(INDEX_CURVE, { color: INDEX_GOLD, width: w(2.5), widthMode, tolerance: 0.04, joint: "bevel", dash, transform: { center: indexPath(7, 1, 0.5, 0, 0.65) } });
1219
- // grid
1220
- recorder.grid(indexPath(8, 0, 0.5, 0, 0.05), [4, 3], [0.25, 0.25], { color: WHITE, width: w(1.5), widthMode, outer: "xz", skew: [0, 0] });
1221
- recorder.grid(indexPath(8, 1, 0.5), [4, 3], [0.25, 0.25], { color: INDEX_BRIGHT_CYAN, width: w(1.5), widthMode, rotation: [Math.PI * 0.5, 0, 0], outer: "xz", skew: [0, 0], normal: INDEX_UP });
1222
- // rect: base, [solid], normal, dashed, normal + dashed
1223
- // `index.gd` passes `Quaternion.IDENTITY` for this column, so the rect lies in the XY plane
1224
- // rather than the ground plane (unlike `rect.gd`, whose own flat basis is the XZ plane).
1225
- let rectRow = 0;
1226
- const rectAt = (row) => rectLoop(indexPath(9, row, 0.5), IDENTITY_BASIS, 0.8, 0.5);
1227
- recorder.lineStrip(rectAt(rectRow++), { color: WHITE, width: w(2), widthMode, joint: "round" }, true);
1228
- if (solids)
1229
- recorder.mesh(rectMesh(indexPath(9, rectRow++, 0.5), IDENTITY_BASIS, 0.8, 0.5), { color: [1, 0.25, 0.25, 0.45], solid: true });
1230
- drawRectNormals(recorder, rectAt(rectRow++), IDENTITY_BASIS, { color: INDEX_BRIGHT_CYAN, width: w(2), widthMode, joint: "bevel" });
1231
- recorder.lineStrip(rectAt(rectRow++), { color: INDEX_GOLD, width: w(2), widthMode, joint: "miter", dash }, true);
1232
- drawRectNormals(recorder, rectAt(rectRow++), IDENTITY_BASIS, { color: INDEX_MINT, width: w(2), widthMode, joint: "round", dash });
1233
- // box: base, [solid], normal, dashed, normal + dashed
1234
- const boxSize = [0.7, 0.7, 0.7];
1235
- let boxRow = 0;
1236
- const wireBox = (row, options) => {
1237
- drawBox(recorder, indexPath(10, row, 0.5), IDENTITY_BASIS, boxSize, options);
1238
- };
1239
- const wireBoxNormals = (row, options) => {
1240
- drawBoxNormals(recorder, indexPath(10, row, 0.5), IDENTITY_BASIS, boxSize, options);
1241
- };
1242
- wireBox(boxRow++, { color: WHITE, width: w(2), widthMode });
1243
- if (solids)
1244
- recorder.mesh(transformMeshByBasis(box(boxSize), indexPath(10, boxRow++, 0.5), IDENTITY_BASIS), { color: [1, 0.45, 0.2, 0.35], solid: true });
1245
- wireBoxNormals(boxRow++, { color: INDEX_BRIGHT_CYAN, width: w(2), widthMode });
1246
- wireBox(boxRow++, { color: INDEX_GOLD, width: w(2), widthMode, dash });
1247
- wireBoxNormals(boxRow++, { color: INDEX_MINT, width: w(2), widthMode, dash });
1248
- // circle: base, [solid], normal, dashed, normal + dashed
1249
- // `Quaternion.IDENTITY` in `index.gd` puts this column in the XY plane as well, so the ring and
1250
- // its solid fan are vertical here while `circle.gd`'s own flat basis stays on the ground.
1251
- let circleRow = 0;
1252
- const circleRing = (row) => circlePoints(indexPath(11, row, 0.5), 0.4, 32, IDENTITY_BASIS);
1253
- recorder.lineStrip(circleRing(circleRow++), { color: WHITE, width: w(2), widthMode, joint: "round" }, true);
1254
- if (solids)
1255
- recorder.mesh(circleSolidMesh(0.4, 32, indexPath(11, circleRow++, 0.5), IDENTITY_BASIS), { color: [0.25, 0.65, 1, 0.45], solid: true });
1256
- drawCircleNormals(recorder, circleRing(circleRow++), 32, IDENTITY_BASIS, { color: INDEX_BRIGHT_CYAN, width: w(2), widthMode, joint: "bevel" });
1257
- recorder.lineStrip(circleRing(circleRow++), { color: INDEX_GOLD, width: w(2), widthMode, joint: "miter", dash }, true);
1258
- drawCircleNormals(recorder, circleRing(circleRow++), 32, IDENTITY_BASIS, { color: INDEX_MINT, width: w(2), widthMode, joint: "round", dash });
1259
- // arc
1260
- const arcAt = (row) => arcPoints(indexPath(12, row, 0.5), 0.45, Math.PI * 1.25, 32, IDENTITY_BASIS);
1261
- recorder.lineStrip(arcAt(0), { color: WHITE, width: w(2), widthMode, joint: "round" }, false);
1262
- drawArcNormals(recorder, arcAt(1), Math.PI * 1.25, 32, IDENTITY_BASIS, { color: INDEX_BRIGHT_CYAN, width: w(2), widthMode, joint: "bevel" });
1263
- recorder.lineStrip(arcAt(2), { color: INDEX_GOLD, width: w(2), widthMode, joint: "miter", dash }, false);
1264
- drawArcNormals(recorder, arcAt(3), Math.PI * 1.25, 32, IDENTITY_BASIS, { color: INDEX_MINT, width: w(2), widthMode, joint: "round", dash });
1265
- // sphere: base, [solid], normal, dashed, normal + dashed
1266
- const sphereAt = (row) => sphereWire(0.4, 32, 2, 2, indexPath(13, row, 0.5), IDENTITY_BASIS);
1267
- const wireSphere = (row, options, normalAware) => {
1268
- const shape = sphereAt(row);
1269
- drawShapeWire(recorder, shape, options, normalAware ? sphereWireNormals(indexPath(13, row, 0.5), shape) : undefined);
1270
- };
1271
- let sphereRow = 0;
1272
- wireSphere(sphereRow++, { color: WHITE, width: w(2), widthMode, joint: "round" }, false);
1273
- if (solids)
1274
- recorder.mesh(sphereSolidMesh(0.4, indexPath(13, sphereRow++, 0.5), IDENTITY_BASIS), { color: [0.35, 0.75, 1, 0.35], solid: true });
1275
- wireSphere(sphereRow++, { color: INDEX_BRIGHT_CYAN, width: w(2), widthMode, joint: "bevel" }, true);
1276
- wireSphere(sphereRow++, { color: INDEX_GOLD, width: w(2), widthMode, joint: "miter", dash }, false);
1277
- wireSphere(sphereRow++, { color: INDEX_MINT, width: w(2), widthMode, joint: "round", dash }, true);
1278
- // cone: base, [solid], normal
1279
- const coneAt = (row) => coneWire(0.35, 0.8, 32, 8, indexPath(14, row, 0.5), IDENTITY_BASIS);
1280
- const wireCone = (row, options, normalAware) => {
1281
- const shape = coneAt(row);
1282
- drawShapeWire(recorder, shape, options, normalAware ? coneWireNormals(shape, 0.35, 0.8, 8, IDENTITY_BASIS) : undefined);
1283
- };
1284
- let coneRow = 0;
1285
- wireCone(coneRow++, { color: WHITE, width: w(2), widthMode, joint: "round" }, false);
1286
- if (solids)
1287
- recorder.mesh(coneSolidMesh(0.35, 0.8, indexPath(14, coneRow++, 0.5), IDENTITY_BASIS), { color: [1, 0.45, 0.25, 0.35], solid: true });
1288
- wireCone(coneRow++, { color: INDEX_BRIGHT_CYAN, width: w(2), widthMode, joint: "bevel" }, true);
1289
- // cylinder: base, [solid], normal
1290
- const cylinderAt = (row) => cylinderWire(0.35, 0.4, 32, 8, indexPath(15, row, 0.5), IDENTITY_BASIS);
1291
- const wireCylinder = (row, options, normalAware) => {
1292
- const shape = cylinderAt(row);
1293
- drawShapeWire(recorder, shape, options, normalAware ? cylinderWireNormals(shape, 0.35, 0.4, 8, IDENTITY_BASIS) : undefined);
1294
- };
1295
- let cylinderRow = 0;
1296
- wireCylinder(cylinderRow++, { color: WHITE, width: w(2), widthMode, joint: "round" }, false);
1297
- if (solids)
1298
- recorder.mesh(cylinderSolidMesh(0.35, 0.4, indexPath(15, cylinderRow++, 0.5), IDENTITY_BASIS), { color: [0.6, 0.45, 1, 0.35], solid: true });
1299
- wireCylinder(cylinderRow++, { color: INDEX_BRIGHT_CYAN, width: w(2), widthMode, joint: "bevel" }, true);
1300
- // capsule: base, normal
1301
- const capsuleAt = (row) => capsuleWire(0.32, 0.45, 32, 2, indexPath(16, row, 0.5), IDENTITY_BASIS);
1302
- const wireCapsule = (row, options, normalAware) => {
1303
- const shape = capsuleAt(row);
1304
- drawShapeWire(recorder, shape, options, normalAware ? capsuleWireNormals(shape, 0.45, indexPath(16, row, 0.5), IDENTITY_BASIS) : undefined);
1305
- };
1306
- wireCapsule(0, { color: WHITE, width: w(2), widthMode, joint: "round" }, false);
1307
- wireCapsule(1, { color: INDEX_BRIGHT_CYAN, width: w(2), widthMode, joint: "bevel" }, true);
1308
- }
1309
- /** `index.gd`: screen-space widths, with the `*_s` solid columns. */
1310
- function indexMatrix(recorder) {
1311
- apiMatrix(recorder, { width: (pixels) => pixels, widthMode: "screen", solids: true });
1312
- }
1313
- // ---------------------------------------------------------------------------
1314
- // Faithful Sphere/Cone/Cylinder/Capsule reference ports.
1315
- //
1316
- // `examples/sphere.gd`, `cone.gd`, `cylinder.gd` and `capsule.gd` share the same
1317
- // structure: per-column centres, a rotation tunnel and (except for capsule) a
1318
- // solid `*_s` column that draws a `from_hsv` mesh plus the normal-aware
1319
- // wireframe. Capsule deliberately has no solid column in the reference.
1320
- //
1321
- // The wireframe topology is the reference's, not a mesh's: `sphere` emits one latitude
1322
- // ring per `y_axis_segment_count` step plus one meridian great circle per
1323
- // `z_axis_segment_count` step, `capsule` emits two rings plus `side_segment_count` closed
1324
- // silhouettes, `cone`/`cylinder` emit their rings plus `side_segment_count` side lines.
1325
- // The generators for that live in `geometry-generators.ts`; this file only places them.
1326
- // ---------------------------------------------------------------------------
1327
- /**
1328
- * Draws a reference shape wireframe.
1329
- *
1330
- * The reference emits one `draw_line_loop` per ring and a `draw_line_list` for the side
1331
- * lines, so a closed path keeps its joints and its accumulated dash phase; drawing the
1332
- * same points as independent segments would restart the dash pattern per edge.
1333
- */
1334
- function drawShapeWire(recorder, shape, options, normals) {
1335
- shape.loops.forEach((loop, index) => {
1336
- // Reference `draw_line_loop_n`: each ring joins and dashes with the normals it carries,
1337
- // while a plain ring keeps the flat style (`normal` is undefined there).
1338
- const loopNormals = normals?.loops?.[index];
1339
- recorder.lineStrip(loop, loopNormals === undefined ? options : { ...options, normals: loopNormals }, true);
1340
- });
1341
- shape.lines.forEach(([a, b], index) => {
1342
- const lineNormal = normals?.lines?.[index];
1343
- recorder.line(a, b, lineNormal === undefined ? options : { ...options, normal: lineNormal });
1344
- });
1345
- }
1346
- /** Reference `_emit_sphere_s` emits a `SphereMesh` (64 radial segments, 32 rings). */
1347
- function sphereSolidMesh(radius, center, basis) {
1348
- return transformMeshByBasis(sphere(radius, 64, 32), center, basis);
1349
- }
1350
- /** Only the cone/cylinder segment tables differ from the shared sphere values. */
1351
- const SHAPE_SEGMENT_VALUES = [3, 4, 8, 16];
1352
- function shapeScene(recorder, id, context) {
1353
- if (id === "sphere")
1354
- sphereShape(recorder, context);
1355
- else if (id === "cone")
1356
- coneShape(recorder);
1357
- else if (id === "cylinder")
1358
- cylinderShape(recorder);
1359
- else
1360
- capsuleShape(recorder);
1361
- }
1362
- // --- Sphere ---------------------------------------------------------------
1363
- const SPHERE_LAYOUT = createLayout(SCENE_LAYOUTS.sphere, SCENE_LAYOUTS.sphere.baseY);
1364
- const SPHERE_BASE_Y = SCENE_LAYOUTS.sphere.baseY;
1365
- const SPHERE_RADIUS = 0.30;
1366
- const SPHERE_BASE_LINE_WIDTH = 2;
1367
- const SPHERE_JOINT_LINE_WIDTH = 8;
1368
- const SPHERE_SEGMENT_COUNT = 48;
1369
- const SPHERE_JOINT_SEGMENT_COUNT = 16;
1370
- const SPHERE_AXIS_SEGMENT_COUNT = 2;
1371
- const SPHERE_Y_AXIS_SEGMENT_VALUES = [0, 1, 2, 6];
1372
- const SPHERE_Z_AXIS_SEGMENT_VALUES = [0, 1, 2, 6];
1373
- const SPHERE_ROTATION_TUNNEL_RADIUS = 0.42;
1374
- const SPHERE_ROTATION_TUNNEL_DEPTH_STEP = 0.16;
1375
- const SPHERE_ROTATION_TUNNEL_SCALE_STEP = 0.05;
1376
- const SPHERE_ROTATION_TUNNEL_Y = 0.35;
1377
- const SPHERE_S_SAMPLE_COUNT = 8;
1378
- const SPHERE_S_TUNNEL_RADIUS = 0.42;
1379
- const SPHERE_S_TUNNEL_DEPTH_STEP = 0.16;
1380
- const SPHERE_S_TUNNEL_SCALE_STEP = 0.05;
1381
- const SPHERE_S_TUNNEL_Y = 0.35;
1382
- /** `_get_column_radius`: the radius row plus the two shrinking tunnels. */
1383
- function sphereColumnRadius(column, row) {
1384
- if (column === 1)
1385
- return RADIUS_VALUES[row];
1386
- if (column === 5)
1387
- return SPHERE_ROTATION_TUNNEL_RADIUS * (1 - row * SPHERE_ROTATION_TUNNEL_SCALE_STEP);
1388
- if (column === 13)
1389
- return SPHERE_S_TUNNEL_RADIUS * (1 - row * SPHERE_S_TUNNEL_SCALE_STEP);
1390
- return SPHERE_RADIUS;
1391
- }
1392
- /** `_get_sphere_center`: the sphere rests on `SPHERE_Y`, so its centre rises with the radius. */
1393
- function sphereCenter(column, row) {
1394
- return referenceOffset([SPHERE_LAYOUT.columnX(column), SPHERE_BASE_Y + sphereColumnRadius(column, row), SPHERE_LAYOUT.rowZ(column, row)]);
1395
- }
1396
- function sphereTunnelCenter(column, row, yOffset, depthStep) {
1397
- const center = sphereCenter(column, row);
1398
- return [center[0], center[1] + yOffset, center[2] - row * depthStep];
1399
- }
1400
- /** `_get_rotation_tunnel_rotation` / `_get_sphere_s_tunnel_rotation` (identical). */
1401
- function sphereTunnelBasis(row) {
1402
- let basis = rotateBasis(IDENTITY_BASIS, [1, 0, 0], -Math.PI * 0.35);
1403
- basis = rotateBasis(basis, [0, 0, 1], row * Math.PI * 0.10);
1404
- return rotateBasis(basis, [0, 1, 0], row * Math.PI * 0.08);
1405
- }
1406
- /** `draw_sphere`: latitude rings (`yAxis`) plus meridian great circles (`zAxis`). */
1407
- function sphereWireAt(radius, segments, yAxis, zAxis, center, basis) {
1408
- return sphereWire(radius, segments, yAxis, zAxis, center, basis);
1409
- }
1410
- /** `draw_sphere_n`: every ring shaded from the sphere centre (`_get_sphere_ring_normals`). */
1411
- function sphereWireNormals(center, shape) {
1412
- return { loops: shape.loops.map((loop) => sphereRingNormals(center, loop)) };
1413
- }
1414
- function sphereShape(recorder, context) {
1415
- const dashOffset = context.time * DASH_OFFSET_SPEED;
1416
- const wire = (shape, options) => drawShapeWire(recorder, shape, options);
1417
- const base = (column, row, segments = SPHERE_SEGMENT_COUNT, yAxis = SPHERE_AXIS_SEGMENT_COUNT, zAxis = SPHERE_AXIS_SEGMENT_COUNT) => sphereWireAt(sphereColumnRadius(column, row), segments, yAxis, zAxis, sphereCenter(column, row), IDENTITY_BASIS);
1418
- // `draw_sphere_n`: every ring (latitude ring and great circle alike) is shaded from the
1419
- // sphere centre, which is exactly what `_get_sphere_ring_normals` computes.
1420
- const wireNormal = (column, row, options, segments = SPHERE_SEGMENT_COUNT, yAxis = SPHERE_AXIS_SEGMENT_COUNT, zAxis = SPHERE_AXIS_SEGMENT_COUNT) => {
1421
- const shape = base(column, row, segments, yAxis, zAxis);
1422
- const center = sphereCenter(column, row);
1423
- drawShapeWire(recorder, shape, options, { loops: shape.loops.map((loop) => sphereRingNormals(center, loop)) });
1424
- };
1425
- // sphere(width)
1426
- WIDTH_VALUES.forEach((width, row) => wire(base(0, row), { color: WHITE, width }));
1427
- // sphere(radius)
1428
- RADIUS_VALUES.forEach((_radius, row) => wire(base(1, row), { color: CYAN, width: SPHERE_BASE_LINE_WIDTH }));
1429
- // sphere(segments)
1430
- SEGMENT_VALUES.forEach((segments, row) => wire(base(2, row, segments), { color: LIME, width: SPHERE_BASE_LINE_WIDTH }));
1431
- // sphere(y axis)
1432
- SPHERE_Y_AXIS_SEGMENT_VALUES.forEach((yAxis, row) => wire(base(3, row, SPHERE_SEGMENT_COUNT, yAxis), { color: GOLD, width: SPHERE_BASE_LINE_WIDTH }));
1433
- // sphere(z axis)
1434
- SPHERE_Z_AXIS_SEGMENT_VALUES.forEach((zAxis, row) => wire(base(4, row, SPHERE_SEGMENT_COUNT, SPHERE_AXIS_SEGMENT_COUNT, zAxis), { color: ORANGE_FULL, width: SPHERE_BASE_LINE_WIDTH }));
1435
- // sphere(rotation)
1436
- for (let row = 0; row < ROTATION_SAMPLE_COUNT; row++) {
1437
- const scale = 1 - row * SPHERE_ROTATION_TUNNEL_SCALE_STEP;
1438
- const shape = sphereWireAt(SPHERE_ROTATION_TUNNEL_RADIUS * scale, SPHERE_SEGMENT_COUNT, SPHERE_AXIS_SEGMENT_COUNT, SPHERE_AXIS_SEGMENT_COUNT, sphereTunnelCenter(5, row, SPHERE_ROTATION_TUNNEL_Y, SPHERE_ROTATION_TUNNEL_DEPTH_STEP), sphereTunnelBasis(row));
1439
- wire(shape, { color: [0.95, 0.75, 0.25, 1 - row * 0.07], width: SPHERE_BASE_LINE_WIDTH });
1440
- }
1441
- // sphere(joint)
1442
- JOINT_VALUES.forEach((joint, row) => wire(base(6, row, SPHERE_JOINT_SEGMENT_COUNT), { color: VIOLET, width: SPHERE_JOINT_LINE_WIDTH, joint }));
1443
- // sphere_d(dash)
1444
- DASH_VALUES.forEach(([dash, space], row) => wire(base(7, row), { color: ORANGE_FULL, width: SPHERE_BASE_LINE_WIDTH, dash: [dash, space, fposmod(dashOffset, dash + space)] }));
1445
- // sphere_d(joint)
1446
- JOINT_VALUES.forEach((joint, row) => wire(base(8, row, SPHERE_JOINT_SEGMENT_COUNT), { color: PINK, width: SPHERE_JOINT_LINE_WIDTH, joint, dash: [0.18, 0.1, fposmod(dashOffset, 0.28)] }));
1447
- // sphere_n
1448
- for (let row = 0; row < NORMAL_SAMPLE_COUNT; row++) {
1449
- wireNormal(9, row, { color: normalSampleColor(row), width: normalSampleWidth(row, SPHERE_BASE_LINE_WIDTH), joint: "round" });
1450
- }
1451
- // sphere_n(joint)
1452
- JOINT_VALUES.forEach((joint, row) => wireNormal(10, row, { color: TEAL, width: SPHERE_JOINT_LINE_WIDTH, joint }, SPHERE_JOINT_SEGMENT_COUNT));
1453
- // sphere_n_d(dash)
1454
- DASH_VALUES.forEach(([dash, space], row) => wireNormal(11, row, { color: TEAL, width: SPHERE_BASE_LINE_WIDTH, dash: [dash, space, fposmod(dashOffset, dash + space)], joint: "round" }));
1455
- // sphere_n_d(joint)
1456
- JOINT_VALUES.forEach((joint, row) => wireNormal(12, row, { color: TEAL, width: SPHERE_JOINT_LINE_WIDTH, joint, dash: [0.18, 0.1, fposmod(dashOffset, 0.28)] }, SPHERE_JOINT_SEGMENT_COUNT));
1457
- // sphere_s(solid): the reference draws the `SphereMesh` and overlays the wireframe.
1458
- for (let row = 0; row < SPHERE_S_SAMPLE_COUNT; row++) {
1459
- const scale = 1 - row * SPHERE_S_TUNNEL_SCALE_STEP;
1460
- const center = sphereTunnelCenter(13, row, SPHERE_S_TUNNEL_Y, SPHERE_S_TUNNEL_DEPTH_STEP);
1461
- const basis = sphereTunnelBasis(row);
1462
- const radius = SPHERE_S_TUNNEL_RADIUS * scale;
1463
- const shape = sphereWireAt(radius, SPHERE_SEGMENT_COUNT, SPHERE_AXIS_SEGMENT_COUNT, SPHERE_AXIS_SEGMENT_COUNT, center, basis);
1464
- recorder.mesh(sphereSolidMesh(radius, center, basis), { color: fromHsv(row / SPHERE_S_SAMPLE_COUNT + 0.6, 0.7, 1, 0.35), solid: true });
1465
- drawShapeWire(recorder, shape, { color: fromHsv(row / SPHERE_S_SAMPLE_COUNT + 0.6, 0.7, 1, 1), width: SPHERE_BASE_LINE_WIDTH }, sphereWireNormals(center, shape));
1466
- }
1467
- }
1468
- // --- Cone -----------------------------------------------------------------
1469
- const CONE_LAYOUT = createLayout(SCENE_LAYOUTS.cone, SCENE_LAYOUTS.cone.baseY);
1470
- const CONE_BASE_Y = SCENE_LAYOUTS.cone.baseY;
1471
- const CONE_RADIUS = 0.28;
1472
- const CONE_HEIGHT = 0.7;
1473
- const CONE_BASE_LINE_WIDTH = 2;
1474
- const CONE_JOINT_LINE_WIDTH = 8;
1475
- const CONE_SEGMENT_COUNT = 16;
1476
- const CONE_SIDE_SEGMENT_COUNT = 8;
1477
- const CONE_JOINT_SEGMENT_COUNT = 8;
1478
- const CONE_JOINT_SIDE_SEGMENT_COUNT = 8;
1479
- const CONE_HEIGHT_VALUES = [0.45, 0.65, 0.85, 1.05];
1480
- const CONE_ROTATION_TUNNEL_RADIUS = 0.34;
1481
- const CONE_ROTATION_TUNNEL_HEIGHT = 0.82;
1482
- const CONE_ROTATION_TUNNEL_DEPTH_STEP = 0.16;
1483
- const CONE_ROTATION_TUNNEL_SCALE_STEP = 0.05;
1484
- const CONE_ROTATION_TUNNEL_Y = 0.35;
1485
- const CONE_S_SAMPLE_COUNT = 8;
1486
- const CONE_S_TUNNEL_RADIUS = 0.34;
1487
- const CONE_S_TUNNEL_HEIGHT = 0.82;
1488
- const CONE_S_TUNNEL_DEPTH_STEP = 0.16;
1489
- const CONE_S_TUNNEL_SCALE_STEP = 0.05;
1490
- const CONE_S_TUNNEL_Y = 0.35;
1491
- /** `_get_column_radius`: the radius row plus the rotation/solid tunnels. */
1492
- function coneColumnRadius(column, row) {
1493
- if (column === 1)
1494
- return RADIUS_VALUES[row];
1495
- if (column === 5)
1496
- return CONE_ROTATION_TUNNEL_RADIUS * (1 - row * CONE_ROTATION_TUNNEL_SCALE_STEP);
1497
- if (column === 9)
1498
- return CONE_S_TUNNEL_RADIUS * (1 - row * CONE_S_TUNNEL_SCALE_STEP);
1499
- return CONE_RADIUS;
1500
- }
1501
- /** `_get_column_height`: the height row plus the rotation/solid tunnels. */
1502
- function coneColumnHeight(column, row) {
1503
- if (column === 2)
1504
- return CONE_HEIGHT_VALUES[row];
1505
- if (column === 5)
1506
- return CONE_ROTATION_TUNNEL_HEIGHT * (1 - row * CONE_ROTATION_TUNNEL_SCALE_STEP);
1507
- if (column === 9)
1508
- return CONE_S_TUNNEL_HEIGHT * (1 - row * CONE_S_TUNNEL_SCALE_STEP);
1509
- return CONE_HEIGHT;
1510
- }
1511
- /** `_get_cone_center`: the cone base stays at `CONE_BASE_Y`, so the centre rises with half the height. */
1512
- function coneCenter(column, row) {
1513
- return referenceOffset([CONE_LAYOUT.columnX(column), CONE_BASE_Y + coneColumnHeight(column, row) * 0.5, CONE_LAYOUT.rowZ(column, row)]);
1514
- }
1515
- function coneTunnelCenter(column, row, yOffset, depthStep) {
1516
- const center = coneCenter(column, row);
1517
- return [center[0], center[1] + yOffset, center[2] - row * depthStep];
1518
- }
1519
- /** `_get_rotation_tunnel_rotation` / `_get_cone_s_tunnel_rotation` (identical). */
1520
- function coneTunnelBasis(row) {
1521
- let basis = rotateBasis(IDENTITY_BASIS, [1, 0, 0], -Math.PI * 0.26);
1522
- basis = rotateBasis(basis, [0, 0, 1], row * Math.PI * 0.10);
1523
- return rotateBasis(basis, [0, 1, 0], row * Math.PI * 0.08);
1524
- }
1525
- function coneWireAt(radius, height, segments, sideSegments, center, basis) {
1526
- return coneWire(radius, height, segments, sideSegments, center, basis);
1527
- }
1528
- /** Reference `_emit_cone_s` emits a `CylinderMesh` (`top_radius = 0`). */
1529
- function coneSolidMesh(radius, height, center, basis) {
1530
- return transformMeshByBasis(cone(radius, height, 64), center, basis);
1531
- }
1532
- /**
1533
- * `draw_cone_n`: the base ring uses `_get_circle_normals` on the cone's base basis, the side
1534
- * lines use `_get_cone_side_line_normals` (one tilted normal per line).
1535
- */
1536
- function coneWireNormals(shape, radius, height, sideSegments, basis) {
1537
- const ringNormals = circleNormals(shape.loops[0]?.length ?? 0, coneBaseBasis(basis));
1538
- return {
1539
- loops: shape.loops.map(() => ringNormals),
1540
- lines: coneSideLineNormals(radius, height, sideSegments, basis),
1541
- };
1542
- }
1543
- function coneShape(recorder) {
1544
- const wire = (shape, options) => drawShapeWire(recorder, shape, options);
1545
- const base = (column, row, segments = CONE_SEGMENT_COUNT, sideSegments = CONE_SIDE_SEGMENT_COUNT) => coneWireAt(coneColumnRadius(column, row), coneColumnHeight(column, row), segments, sideSegments, coneCenter(column, row), IDENTITY_BASIS);
1546
- const wireNormal = (column, row, options, segments = CONE_SEGMENT_COUNT, sideSegments = CONE_SIDE_SEGMENT_COUNT) => {
1547
- const shape = base(column, row, segments, sideSegments);
1548
- drawShapeWire(recorder, shape, options, coneWireNormals(shape, coneColumnRadius(column, row), coneColumnHeight(column, row), sideSegments, IDENTITY_BASIS));
1549
- };
1550
- // cone(width)
1551
- WIDTH_VALUES.forEach((width, row) => wire(base(0, row), { color: WHITE, width }));
1552
- // cone(radius)
1553
- RADIUS_VALUES.forEach((_radius, row) => wire(base(1, row), { color: CYAN, width: CONE_BASE_LINE_WIDTH }));
1554
- // cone(height)
1555
- CONE_HEIGHT_VALUES.forEach((_height, row) => wire(base(2, row), { color: LIME, width: CONE_BASE_LINE_WIDTH }));
1556
- // cone(segments)
1557
- SHAPE_SEGMENT_VALUES.forEach((segments, row) => wire(base(3, row, segments), { color: GOLD, width: CONE_BASE_LINE_WIDTH }));
1558
- // cone(side segments)
1559
- SHAPE_SEGMENT_VALUES.forEach((sideSegments, row) => wire(base(4, row, CONE_SEGMENT_COUNT, sideSegments), { color: ORANGE_FULL, width: CONE_BASE_LINE_WIDTH }));
1560
- // cone(rotation)
1561
- for (let row = 0; row < ROTATION_SAMPLE_COUNT; row++) {
1562
- const scale = 1 - row * CONE_ROTATION_TUNNEL_SCALE_STEP;
1563
- const shape = coneWireAt(CONE_ROTATION_TUNNEL_RADIUS * scale, CONE_ROTATION_TUNNEL_HEIGHT * scale, CONE_SEGMENT_COUNT, CONE_SIDE_SEGMENT_COUNT, coneTunnelCenter(5, row, CONE_ROTATION_TUNNEL_Y, CONE_ROTATION_TUNNEL_DEPTH_STEP), coneTunnelBasis(row));
1564
- wire(shape, { color: [0.95, 0.75, 0.25, 1 - row * 0.07], width: CONE_BASE_LINE_WIDTH });
1565
- }
1566
- // cone(joint)
1567
- JOINT_VALUES.forEach((joint, row) => wire(base(6, row, CONE_JOINT_SEGMENT_COUNT, CONE_JOINT_SIDE_SEGMENT_COUNT), { color: VIOLET, width: CONE_JOINT_LINE_WIDTH, joint }));
1568
- // cone_n
1569
- for (let row = 0; row < NORMAL_SAMPLE_COUNT; row++) {
1570
- wireNormal(7, row, { color: normalSampleColor(row), width: normalSampleWidth(row, CONE_BASE_LINE_WIDTH), joint: "round" });
1571
- }
1572
- // cone_n(joint)
1573
- JOINT_VALUES.forEach((joint, row) => wireNormal(8, row, { color: TEAL, width: CONE_JOINT_LINE_WIDTH, joint }, CONE_JOINT_SEGMENT_COUNT, CONE_JOINT_SIDE_SEGMENT_COUNT));
1574
- // cone_s(solid): the reference also overlays the normal-aware wireframe.
1575
- for (let row = 0; row < CONE_S_SAMPLE_COUNT; row++) {
1576
- const scale = 1 - row * CONE_S_TUNNEL_SCALE_STEP;
1577
- const center = coneTunnelCenter(9, row, CONE_S_TUNNEL_Y, CONE_S_TUNNEL_DEPTH_STEP);
1578
- const basis = coneTunnelBasis(row);
1579
- const radius = CONE_S_TUNNEL_RADIUS * scale;
1580
- const height = CONE_S_TUNNEL_HEIGHT * scale;
1581
- const shape = coneWireAt(radius, height, CONE_SEGMENT_COUNT, CONE_SIDE_SEGMENT_COUNT, center, basis);
1582
- recorder.mesh(coneSolidMesh(radius, height, center, basis), { color: fromHsv(row / CONE_S_SAMPLE_COUNT + 0.6, 0.7, 1, 0.35), solid: true });
1583
- drawShapeWire(recorder, shape, { color: fromHsv(row / CONE_S_SAMPLE_COUNT + 0.6, 0.7, 1, 1), width: CONE_BASE_LINE_WIDTH, joint: "round" }, coneWireNormals(shape, radius, height, CONE_SIDE_SEGMENT_COUNT, basis));
1584
- }
1585
- }
1586
- // --- Cylinder -------------------------------------------------------------
1587
- const CYLINDER_LAYOUT = createLayout(SCENE_LAYOUTS.cylinder, SCENE_LAYOUTS.cylinder.baseY);
1588
- const CYLINDER_BASE_Y = SCENE_LAYOUTS.cylinder.baseY;
1589
- const CYLINDER_RADIUS = 0.28;
1590
- const CYLINDER_HALF_HEIGHT = 0.35;
1591
- const CYLINDER_BASE_LINE_WIDTH = 2;
1592
- const CYLINDER_JOINT_LINE_WIDTH = 8;
1593
- const CYLINDER_SEGMENT_COUNT = 16;
1594
- const CYLINDER_SIDE_SEGMENT_COUNT = 8;
1595
- const CYLINDER_JOINT_SEGMENT_COUNT = 8;
1596
- const CYLINDER_JOINT_SIDE_SEGMENT_COUNT = 8;
1597
- const CYLINDER_HALF_HEIGHT_VALUES = [0.225, 0.325, 0.425, 0.525];
1598
- const CYLINDER_ROTATION_TUNNEL_RADIUS = 0.34;
1599
- const CYLINDER_ROTATION_TUNNEL_HALF_HEIGHT = 0.41;
1600
- const CYLINDER_ROTATION_TUNNEL_DEPTH_STEP = 0.16;
1601
- const CYLINDER_ROTATION_TUNNEL_SCALE_STEP = 0.05;
1602
- const CYLINDER_ROTATION_TUNNEL_Y = 0.35;
1603
- const CYLINDER_S_SAMPLE_COUNT = 8;
1604
- const CYLINDER_S_TUNNEL_RADIUS = 0.34;
1605
- const CYLINDER_S_TUNNEL_HALF_HEIGHT = 0.41;
1606
- const CYLINDER_S_TUNNEL_DEPTH_STEP = 0.16;
1607
- const CYLINDER_S_TUNNEL_SCALE_STEP = 0.05;
1608
- const CYLINDER_S_TUNNEL_Y = 0.35;
1609
- /** `_get_column_radius`: the radius row plus the rotation/solid tunnels. */
1610
- function cylinderColumnRadius(column, row) {
1611
- if (column === 1)
1612
- return RADIUS_VALUES[row];
1613
- if (column === 5)
1614
- return CYLINDER_ROTATION_TUNNEL_RADIUS * (1 - row * CYLINDER_ROTATION_TUNNEL_SCALE_STEP);
1615
- if (column === 9)
1616
- return CYLINDER_S_TUNNEL_RADIUS * (1 - row * CYLINDER_S_TUNNEL_SCALE_STEP);
1617
- return CYLINDER_RADIUS;
1618
- }
1619
- /** `_get_column_half_height`: the half-height row plus the rotation/solid tunnels. */
1620
- function cylinderColumnHalfHeight(column, row) {
1621
- if (column === 2)
1622
- return CYLINDER_HALF_HEIGHT_VALUES[row];
1623
- if (column === 5)
1624
- return CYLINDER_ROTATION_TUNNEL_HALF_HEIGHT * (1 - row * CYLINDER_ROTATION_TUNNEL_SCALE_STEP);
1625
- if (column === 9)
1626
- return CYLINDER_S_TUNNEL_HALF_HEIGHT * (1 - row * CYLINDER_S_TUNNEL_SCALE_STEP);
1627
- return CYLINDER_HALF_HEIGHT;
1628
- }
1629
- /** `_get_cylinder_center`: the cylinder base stays at `CYLINDER_BASE_Y`. */
1630
- function cylinderCenter(column, row) {
1631
- return referenceOffset([CYLINDER_LAYOUT.columnX(column), CYLINDER_BASE_Y + cylinderColumnHalfHeight(column, row), CYLINDER_LAYOUT.rowZ(column, row)]);
1632
- }
1633
- function cylinderTunnelCenter(column, row, yOffset, depthStep) {
1634
- const center = cylinderCenter(column, row);
1635
- return [center[0], center[1] + yOffset, center[2] - row * depthStep];
1636
- }
1637
- /** `_get_rotation_tunnel_rotation` / `_get_cylinder_s_tunnel_rotation` (identical). */
1638
- function cylinderTunnelBasis(row) {
1639
- let basis = rotateBasis(IDENTITY_BASIS, [1, 0, 0], -Math.PI * 0.26);
1640
- basis = rotateBasis(basis, [0, 0, 1], row * Math.PI * 0.10);
1641
- return rotateBasis(basis, [0, 1, 0], row * Math.PI * 0.08);
1642
- }
1643
- function cylinderWireAt(radius, halfHeight, segments, sideSegments, center, basis) {
1644
- return cylinderWire(radius, halfHeight, segments, sideSegments, center, basis);
1645
- }
1646
- /** Reference `_emit_cylinder_s` emits a `CylinderMesh`. */
1647
- function cylinderSolidMesh(radius, halfHeight, center, basis) {
1648
- return transformMeshByBasis(cylinder(radius, halfHeight, 64), center, basis);
1649
- }
1650
- /**
1651
- * `draw_cylinder_n`: both rings use `_get_circle_normals` on the ring basis, the wall lines
1652
- * use `_get_cylinder_side_line_normals` (one radial normal per line).
1653
- */
1654
- function cylinderWireNormals(shape, radius, halfHeight, sideSegments, basis) {
1655
- const ringNormals = circleNormals(shape.loops[0]?.length ?? 0, coneBaseBasis(basis));
1656
- return {
1657
- loops: shape.loops.map(() => ringNormals),
1658
- lines: cylinderSideLineNormals(radius, halfHeight, sideSegments, basis),
1659
- };
1660
- }
1661
- function cylinderShape(recorder) {
1662
- const wire = (shape, options) => drawShapeWire(recorder, shape, options);
1663
- const base = (column, row, segments = CYLINDER_SEGMENT_COUNT, sideSegments = CYLINDER_SIDE_SEGMENT_COUNT) => cylinderWireAt(cylinderColumnRadius(column, row), cylinderColumnHalfHeight(column, row), segments, sideSegments, cylinderCenter(column, row), IDENTITY_BASIS);
1664
- const wireNormal = (column, row, options, segments = CYLINDER_SEGMENT_COUNT, sideSegments = CYLINDER_SIDE_SEGMENT_COUNT) => {
1665
- const shape = base(column, row, segments, sideSegments);
1666
- drawShapeWire(recorder, shape, options, cylinderWireNormals(shape, cylinderColumnRadius(column, row), cylinderColumnHalfHeight(column, row), sideSegments, IDENTITY_BASIS));
1667
- };
1668
- // cylinder(width)
1669
- WIDTH_VALUES.forEach((width, row) => wire(base(0, row), { color: WHITE, width }));
1670
- // cylinder(radius)
1671
- RADIUS_VALUES.forEach((_radius, row) => wire(base(1, row), { color: CYAN, width: CYLINDER_BASE_LINE_WIDTH }));
1672
- // cylinder(half height)
1673
- CYLINDER_HALF_HEIGHT_VALUES.forEach((_halfHeight, row) => wire(base(2, row), { color: LIME, width: CYLINDER_BASE_LINE_WIDTH }));
1674
- // cylinder(segments)
1675
- SHAPE_SEGMENT_VALUES.forEach((segments, row) => wire(base(3, row, segments), { color: GOLD, width: CYLINDER_BASE_LINE_WIDTH }));
1676
- // cylinder(side segments)
1677
- SHAPE_SEGMENT_VALUES.forEach((sideSegments, row) => wire(base(4, row, CYLINDER_SEGMENT_COUNT, sideSegments), { color: ORANGE_FULL, width: CYLINDER_BASE_LINE_WIDTH }));
1678
- // cylinder(rotation)
1679
- for (let row = 0; row < ROTATION_SAMPLE_COUNT; row++) {
1680
- const scale = 1 - row * CYLINDER_ROTATION_TUNNEL_SCALE_STEP;
1681
- const shape = cylinderWireAt(CYLINDER_ROTATION_TUNNEL_RADIUS * scale, CYLINDER_ROTATION_TUNNEL_HALF_HEIGHT * scale, CYLINDER_SEGMENT_COUNT, CYLINDER_SIDE_SEGMENT_COUNT, cylinderTunnelCenter(5, row, CYLINDER_ROTATION_TUNNEL_Y, CYLINDER_ROTATION_TUNNEL_DEPTH_STEP), cylinderTunnelBasis(row));
1682
- wire(shape, { color: [0.95, 0.75, 0.25, 1 - row * 0.07], width: CYLINDER_BASE_LINE_WIDTH });
1683
- }
1684
- // cylinder(joint)
1685
- JOINT_VALUES.forEach((joint, row) => wire(base(6, row, CYLINDER_JOINT_SEGMENT_COUNT, CYLINDER_JOINT_SIDE_SEGMENT_COUNT), { color: VIOLET, width: CYLINDER_JOINT_LINE_WIDTH, joint }));
1686
- // cylinder_n
1687
- for (let row = 0; row < NORMAL_SAMPLE_COUNT; row++) {
1688
- wireNormal(7, row, { color: normalSampleColor(row), width: normalSampleWidth(row, CYLINDER_BASE_LINE_WIDTH), joint: "round" });
1689
- }
1690
- // cylinder_n(joint)
1691
- JOINT_VALUES.forEach((joint, row) => wireNormal(8, row, { color: TEAL, width: CYLINDER_JOINT_LINE_WIDTH, joint }, CYLINDER_JOINT_SEGMENT_COUNT, CYLINDER_JOINT_SIDE_SEGMENT_COUNT));
1692
- // cylinder_s(solid): the reference also overlays the normal-aware wireframe.
1693
- for (let row = 0; row < CYLINDER_S_SAMPLE_COUNT; row++) {
1694
- const scale = 1 - row * CYLINDER_S_TUNNEL_SCALE_STEP;
1695
- const center = cylinderTunnelCenter(9, row, CYLINDER_S_TUNNEL_Y, CYLINDER_S_TUNNEL_DEPTH_STEP);
1696
- const basis = cylinderTunnelBasis(row);
1697
- const radius = CYLINDER_S_TUNNEL_RADIUS * scale;
1698
- const halfHeight = CYLINDER_S_TUNNEL_HALF_HEIGHT * scale;
1699
- const shape = cylinderWireAt(radius, halfHeight, CYLINDER_SEGMENT_COUNT, CYLINDER_SIDE_SEGMENT_COUNT, center, basis);
1700
- recorder.mesh(cylinderSolidMesh(radius, halfHeight, center, basis), { color: fromHsv(row / CYLINDER_S_SAMPLE_COUNT + 0.6, 0.7, 1, 0.35), solid: true });
1701
- drawShapeWire(recorder, shape, { color: fromHsv(row / CYLINDER_S_SAMPLE_COUNT + 0.6, 0.7, 1, 1), width: CYLINDER_BASE_LINE_WIDTH, joint: "round" }, cylinderWireNormals(shape, radius, halfHeight, CYLINDER_SIDE_SEGMENT_COUNT, basis));
1702
- }
1703
- }
1704
- // --- Capsule --------------------------------------------------------------
1705
- const CAPSULE_LAYOUT = createLayout(SCENE_LAYOUTS.capsule, SCENE_LAYOUTS.capsule.baseY);
1706
- const CAPSULE_BASE_Y = SCENE_LAYOUTS.capsule.baseY;
1707
- const CAPSULE_RADIUS = 0.28;
1708
- const CAPSULE_HALF_HEIGHT = 0.225;
1709
- const CAPSULE_BASE_LINE_WIDTH = 2;
1710
- const CAPSULE_JOINT_LINE_WIDTH = 8;
1711
- const CAPSULE_SEGMENT_COUNT = 32;
1712
- const CAPSULE_SIDE_SEGMENT_COUNT = 2;
1713
- const CAPSULE_JOINT_SEGMENT_COUNT = 12;
1714
- const CAPSULE_JOINT_SIDE_SEGMENT_COUNT = 4;
1715
- const CAPSULE_RADIUS_VALUES = [0.16, 0.24, 0.32, 0.40];
1716
- const CAPSULE_HALF_HEIGHT_VALUES = [0.0, 0.125, 0.225, 0.35];
1717
- const CAPSULE_SIDE_SEGMENT_VALUES = [1, 2, 4, 8];
1718
- const CAPSULE_ROTATION_TUNNEL_RADIUS = 0.34;
1719
- const CAPSULE_ROTATION_TUNNEL_HALF_HEIGHT = 0.275;
1720
- const CAPSULE_ROTATION_TUNNEL_DEPTH_STEP = 0.16;
1721
- const CAPSULE_ROTATION_TUNNEL_SCALE_STEP = 0.05;
1722
- const CAPSULE_ROTATION_TUNNEL_Y = 0.35;
1723
- /** `_get_column_radius`: the radius row plus the rotation tunnel. */
1724
- function capsuleColumnRadius(column, row) {
1725
- if (column === 1)
1726
- return CAPSULE_RADIUS_VALUES[row];
1727
- if (column === 5)
1728
- return CAPSULE_ROTATION_TUNNEL_RADIUS * (1 - row * CAPSULE_ROTATION_TUNNEL_SCALE_STEP);
1729
- return CAPSULE_RADIUS;
1730
- }
1731
- /** `_get_column_half_height`: the half-height row plus the rotation tunnel. */
1732
- function capsuleColumnHalfHeight(column, row) {
1733
- if (column === 2)
1734
- return CAPSULE_HALF_HEIGHT_VALUES[row];
1735
- if (column === 5)
1736
- return CAPSULE_ROTATION_TUNNEL_HALF_HEIGHT * (1 - row * CAPSULE_ROTATION_TUNNEL_SCALE_STEP);
1737
- return CAPSULE_HALF_HEIGHT;
1738
- }
1739
- /** `_get_capsule_center`: base at `CAPSULE_BASE_Y`, so the centre rises by half height plus radius. */
1740
- function capsuleCenter(column, row) {
1741
- const y = CAPSULE_BASE_Y + capsuleColumnHalfHeight(column, row) + capsuleColumnRadius(column, row);
1742
- return referenceOffset([CAPSULE_LAYOUT.columnX(column), y, CAPSULE_LAYOUT.rowZ(column, row)]);
1743
- }
1744
- function capsuleTunnelCenter(column, row, yOffset, depthStep) {
1745
- const center = capsuleCenter(column, row);
1746
- return [center[0], center[1] + yOffset, center[2] - row * depthStep];
1747
- }
1748
- /** `_get_rotation_tunnel_rotation`. */
1749
- function capsuleTunnelBasis(row) {
1750
- let basis = rotateBasis(IDENTITY_BASIS, [1, 0, 0], -Math.PI * 0.28);
1751
- basis = rotateBasis(basis, [0, 0, 1], row * Math.PI * 0.10);
1752
- return rotateBasis(basis, [0, 1, 0], row * Math.PI * 0.08);
1753
- }
1754
- function capsuleWireAt(radius, halfHeight, segments, sideSegments, center, basis) {
1755
- return capsuleWire(radius, halfHeight, segments, sideSegments, center, basis);
1756
- }
1757
- /**
1758
- * `draw_capsule_n`: cap rings use `_get_circle_normals` and the silhouettes
1759
- * `_get_capsule_side_outline_normals`. Both reduce to the same formula here -- an outline
1760
- * segment midpoint projected back onto the cap axis -- because a ring midpoint projects onto
1761
- * its own cap centre, so one helper covers every loop in the reference order.
1762
- */
1763
- function capsuleWireNormals(shape, halfHeight, center, basis) {
1764
- const axis = normalizeVector(basis.y);
1765
- const bottomCenter = offsetAlong(center, axis, -halfHeight);
1766
- const topCenter = offsetAlong(center, axis, halfHeight);
1767
- return { loops: shape.loops.map((loop) => capsuleOutlineNormals(bottomCenter, topCenter, axis, loop)) };
1768
- }
1769
- function capsuleShape(recorder) {
1770
- const wire = (shape, options) => drawShapeWire(recorder, shape, options);
1771
- const base = (column, row, segments = CAPSULE_SEGMENT_COUNT, sideSegments = CAPSULE_SIDE_SEGMENT_COUNT) => capsuleWireAt(capsuleColumnRadius(column, row), capsuleColumnHalfHeight(column, row), segments, sideSegments, capsuleCenter(column, row), IDENTITY_BASIS);
1772
- const wireNormal = (column, row, options, segments = CAPSULE_SEGMENT_COUNT, sideSegments = CAPSULE_SIDE_SEGMENT_COUNT) => {
1773
- const shape = base(column, row, segments, sideSegments);
1774
- drawShapeWire(recorder, shape, options, capsuleWireNormals(shape, capsuleColumnHalfHeight(column, row), capsuleCenter(column, row), IDENTITY_BASIS));
1775
- };
1776
- // capsule(width)
1777
- WIDTH_VALUES.forEach((width, row) => wire(base(0, row), { color: WHITE, width }));
1778
- // capsule(radius)
1779
- CAPSULE_RADIUS_VALUES.forEach((_radius, row) => wire(base(1, row), { color: CYAN, width: CAPSULE_BASE_LINE_WIDTH }));
1780
- // capsule(half height)
1781
- CAPSULE_HALF_HEIGHT_VALUES.forEach((_halfHeight, row) => wire(base(2, row), { color: LIME, width: CAPSULE_BASE_LINE_WIDTH }));
1782
- // capsule(segments)
1783
- SEGMENT_VALUES.forEach((segments, row) => wire(base(3, row, segments), { color: GOLD, width: CAPSULE_BASE_LINE_WIDTH }));
1784
- // capsule(side segments)
1785
- CAPSULE_SIDE_SEGMENT_VALUES.forEach((sideSegments, row) => wire(base(4, row, CAPSULE_SEGMENT_COUNT, sideSegments), { color: PINK, width: CAPSULE_BASE_LINE_WIDTH }));
1786
- // capsule(rotation)
1787
- for (let row = 0; row < ROTATION_SAMPLE_COUNT; row++) {
1788
- const scale = 1 - row * CAPSULE_ROTATION_TUNNEL_SCALE_STEP;
1789
- const shape = capsuleWireAt(CAPSULE_ROTATION_TUNNEL_RADIUS * scale, CAPSULE_ROTATION_TUNNEL_HALF_HEIGHT * scale, CAPSULE_SEGMENT_COUNT, CAPSULE_SIDE_SEGMENT_COUNT, capsuleTunnelCenter(5, row, CAPSULE_ROTATION_TUNNEL_Y, CAPSULE_ROTATION_TUNNEL_DEPTH_STEP), capsuleTunnelBasis(row));
1790
- wire(shape, { color: [0.95, 0.75, 0.25, 1 - row * 0.07], width: CAPSULE_BASE_LINE_WIDTH });
1791
- }
1792
- // capsule(joint)
1793
- JOINT_VALUES.forEach((joint, row) => wire(base(6, row, CAPSULE_JOINT_SEGMENT_COUNT, CAPSULE_JOINT_SIDE_SEGMENT_COUNT), { color: VIOLET, width: CAPSULE_JOINT_LINE_WIDTH, joint }));
1794
- // capsule_n
1795
- for (let row = 0; row < NORMAL_SAMPLE_COUNT; row++) {
1796
- wireNormal(7, row, { color: normalSampleColor(row), width: normalSampleWidth(row, CAPSULE_BASE_LINE_WIDTH), joint: "round" });
1797
- }
1798
- // capsule_n(joint)
1799
- JOINT_VALUES.forEach((joint, row) => wireNormal(8, row, { color: TEAL, width: CAPSULE_JOINT_LINE_WIDTH, joint }, CAPSULE_JOINT_SEGMENT_COUNT, CAPSULE_JOINT_SIDE_SEGMENT_COUNT));
1800
- }
1801
- /**
1802
- * Encoding, raster-scale and placement matrix.
1803
- *
1804
- * Not a reference port — the reference has no text API, so this is vgpu's own capability.
1805
- * Two matrices share one view, because the
1806
- * placement space changes how much of an encoding's quality survives:
1807
- *
1808
- * - a **world-billboard** matrix, six glyph sources by three font sizes — the full encoding axis,
1809
- * drawn facing the camera;
1810
- * - a **world-plane** matrix, the same three font sizes under three representative sources, drawn on
1811
- * a fixed plane so it foreshortens as the camera orbits.
1812
- *
1813
- * The layout follows the line/circle matrices: one axis enumerates the variants and the other the
1814
- * values, and every cell is a sample. It is **transposed** relative to those scenes because text
1815
- * grows sideways — six source columns would be wider than any camera wants to frame. So reading down
1816
- * a column compares encodings at one size, and reading across a row compares sizes under one encoding.
1817
- *
1818
- * Every row floats above the grid plane. The grid is a full-screen translucent plane that writes
1819
- * depth to match the reference, so a block crossing `y = 0` would be clipped along the grid lines.
1820
- */
1821
- function textCompareScene(recorder) {
1822
- const sizes = [16, 24, 36];
1823
- const pixelSize = 0.022;
1824
- const sample = "Handgloves";
1825
- const groups = [
1826
- {
1827
- space: "world-billboard",
1828
- title: "world-billboard",
1829
- labelX: -0.6,
1830
- columnX: [0, 5.0, 10.0],
1831
- firstRowY: 10.4,
1832
- rows: [
1833
- ["bitmap", "bitmap 1x"],
1834
- ["bitmap4x", "bitmap 4x"],
1835
- ["sdf", "sdf 1x"],
1836
- ["sdf4x", "sdf 4x"],
1837
- ["msdf", "msdf outline"],
1838
- ["baked", "msdf baked"],
1839
- ],
1840
- },
1841
- {
1842
- space: "world-plane",
1843
- title: "world-plane",
1844
- // Side by side rather than stacked, with a wide gutter: the camera frames the matrix by width and
1845
- // height together, so a wide, short layout leaves room for the rows to breathe, and the gutter
1846
- // keeps the leaning plane group from sweeping back through the billboard one. The rows match the
1847
- // billboard group one for one, so a row can be read across both spaces.
1848
- labelX: 26.0,
1849
- columnX: [26.6, 31.6, 36.6],
1850
- firstRowY: 10.4,
1851
- rows: [
1852
- ["bitmap", "bitmap 1x"],
1853
- ["bitmap4x", "bitmap 4x"],
1854
- ["sdf", "sdf 1x"],
1855
- ["sdf4x", "sdf 4x"],
1856
- ["msdf", "msdf outline"],
1857
- ["baked", "msdf baked"],
1858
- ],
1859
- },
1860
- ];
1861
- // Height costs framing distance just as width does, so the rows stay as tight as they can while still
1862
- // clearing one another. Screen size of the glyphs is set by how much of the bounds the matrix fills,
1863
- // and scaling the whole thing does not change that — only drawing less of it would.
1864
- const rowStep = 1.45;
1865
- const titleY = 12.2;
1866
- for (const group of groups) {
1867
- recorder.text([group.labelX - 5.6, titleY, 0], group.title, {
1868
- fontSize: 24,
1869
- pixelSize,
1870
- space: "world-billboard",
1871
- source: "baked",
1872
- anchor: "center-right",
1873
- color: [1, 0.85, 0.4, 1],
1874
- });
1875
- for (let column = 0; column < sizes.length; column++) {
1876
- recorder.text([group.columnX[column], titleY, 0], `${sizes[column]}px`, {
1877
- fontSize: 22,
1878
- pixelSize,
1879
- space: "world-billboard",
1880
- source: "baked",
1881
- anchor: "top-left",
1882
- color: [0.6, 0.8, 1, 1],
1883
- });
1884
- }
1885
- for (let row = 0; row < group.rows.length; row++) {
1886
- const [source, label] = group.rows[row];
1887
- const y = group.firstRowY - row * rowStep;
1888
- // The row label always faces the camera so the row stays identifiable, and only the samples
1889
- // follow the space under test. It is drawn by its own source, so the label column doubles as a
1890
- // sample.
1891
- recorder.text([group.labelX, y, 0], label, {
1892
- fontSize: 20,
1893
- pixelSize,
1894
- space: "world-billboard",
1895
- source,
1896
- anchor: "center-right",
1897
- color: [1, 1, 1, 1],
1898
- });
1899
- for (let column = 0; column < sizes.length; column++) {
1900
- recorder.text([group.columnX[column], y, 0], sample, {
1901
- fontSize: sizes[column],
1902
- pixelSize,
1903
- space: group.space,
1904
- source,
1905
- anchor: "center-left",
1906
- color: [1, 1, 1, 1],
1907
- });
1908
- }
1909
- }
1910
- }
1911
- }
1912
- export function buildScene(id, recorder, time, lineJoint) {
1913
- // The 2D pages live in their own module and own their camera space entirely.
1914
- if (buildScene2D(id, recorder, time))
1915
- return;
1916
- const context = lineJoint === undefined ? { time } : { time, lineJoint };
1917
- // Reference: every example's first `_process()` line is
1918
- // `draw_axes(Vector3.ZERO, IDENTITY, 0.75, 2.0)`. `DebugDraw3D` is a child of the scene
1919
- // root, so the local `ZERO` picks up the capture harness's scene translation and the axes
1920
- // land on the shifted origin. `_validate_global_basis()` only rejects rotation/scale, which
1921
- // is what allows that translation. The axes are unconditional scene content, not a
1922
- // toggle: `EndlessGrid3D` and `DebugDraw3D` are sibling nodes that never know about each
1923
- // other, so the grid has no bearing on whether the axes show.
1924
- // The screen-space text pages are their own content in pixel space, so the world axes would only be
1925
- // clutter in front of them; `text-compare` keeps its axes, being a world-space matrix.
1926
- const screenTextPage = id === "text-encodings" ||
1927
- id === "text-scale" ||
1928
- id === "text-world-zoom" ||
1929
- id === "text-layout" ||
1930
- id === "text-anchor" ||
1931
- id === "text-cjk";
1932
- const selfAxes = screenTextPage || id === "index" || id === "grid" || id === "world_space_width";
1933
- // index/grid/world_space_width draw their own translated origin axes (and
1934
- // world_space_width needs world-space widths), so they opt out of the generic one.
1935
- if (!selfAxes)
1936
- recorder.axes(referenceOffset([0, 0, 0]), AXES_LENGTH, 2);
1937
- switch (id) {
1938
- case "index":
1939
- indexMatrix(recorder);
1940
- break;
1941
- case "line":
1942
- lineMatrix(recorder, context);
1943
- break;
1944
- case "arrow":
1945
- arrowMatrix(recorder, context);
1946
- break;
1947
- case "axes":
1948
- axesMatrix(recorder, context);
1949
- break;
1950
- case "grid":
1951
- gridMatrix(recorder);
1952
- break;
1953
- case "rect":
1954
- rectMatrix(recorder, context);
1955
- break;
1956
- case "box":
1957
- boxMatrix(recorder, context);
1958
- break;
1959
- case "circle":
1960
- circleMatrix(recorder, context);
1961
- break;
1962
- case "arc":
1963
- arcMatrix(recorder, context);
1964
- break;
1965
- case "curve":
1966
- curveMatrix(recorder, context);
1967
- break;
1968
- case "sphere":
1969
- case "cone":
1970
- case "cylinder":
1971
- case "capsule":
1972
- shapeScene(recorder, id, context);
1973
- break;
1974
- case "world_space_width":
1975
- worldWidthMatrix(recorder);
1976
- break;
1977
- case "text-compare":
1978
- textCompareScene(recorder);
1979
- break;
1980
- case "text-encodings":
1981
- case "text-scale":
1982
- case "text-world-zoom":
1983
- case "text-layout":
1984
- case "text-anchor":
1985
- case "text-cjk":
1986
- textScene(id, recorder);
1987
- break;
1988
- default: break;
1989
- }
1990
- }
1991
- /** `world_space_width.gd`: the same matrix with world-space widths and no solid columns. */
1992
- function worldWidthMatrix(recorder) {
1993
- apiMatrix(recorder, { width: (pixels) => pixels * WSW_LINE_WIDTH_SCALE, widthMode: "world", solids: false });
1994
- }