vgpu-render 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +605 -2
- package/dist/assets/fira-mono-medium-subset.d.ts +4 -0
- package/dist/assets/fira-mono-medium-subset.js +889 -0
- package/dist/assets/fira-mono-msdf-atlas.d.ts +13 -0
- package/dist/assets/fira-mono-msdf-atlas.js +6222 -0
- package/dist/camera-2d.d.ts +43 -0
- package/dist/camera-2d.js +75 -0
- package/dist/color-space.d.ts +32 -0
- package/dist/color-space.js +45 -0
- package/dist/curve.d.ts +16 -0
- package/dist/curve.js +46 -0
- package/dist/debug-draw-3d.d.ts +14 -0
- package/dist/debug-draw-3d.js +36 -0
- package/dist/frame-draw-list-2d.d.ts +177 -0
- package/dist/frame-draw-list-2d.js +378 -0
- package/dist/frame-draw-list.d.ts +413 -0
- package/dist/frame-draw-list.js +556 -0
- package/dist/geometry-generators.d.ts +101 -0
- package/dist/geometry-generators.js +481 -0
- package/dist/gpu-debug-draw-2d.d.ts +48 -0
- package/dist/gpu-debug-draw-2d.js +450 -0
- package/dist/gpu-debug-draw-3d.d.ts +52 -0
- package/dist/gpu-debug-draw-3d.js +892 -0
- package/dist/gpu-instancing.d.ts +34 -0
- package/dist/gpu-instancing.js +93 -0
- package/dist/gpu-text-3d.d.ts +98 -0
- package/dist/gpu-text-3d.js +514 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +20 -0
- package/dist/infinite-grid-2d.d.ts +73 -0
- package/dist/infinite-grid-2d.js +253 -0
- package/dist/infinite-grid-3d.d.ts +134 -0
- package/dist/infinite-grid-3d.js +407 -0
- package/dist/mesh-batch-3d.d.ts +10 -0
- package/dist/mesh-batch-3d.js +67 -0
- package/dist/shape2d-shader.d.ts +14 -0
- package/dist/shape2d-shader.js +288 -0
- package/dist/text-atlas.d.ts +176 -0
- package/dist/text-atlas.js +498 -0
- package/dist/text-font.d.ts +190 -0
- package/dist/text-font.js +261 -0
- package/dist/text-layout.d.ts +175 -0
- package/dist/text-layout.js +390 -0
- package/dist/text-msdf-baked.d.ts +41 -0
- package/dist/text-msdf-baked.js +117 -0
- package/dist/text-msdf-font.d.ts +85 -0
- package/dist/text-msdf-font.js +159 -0
- package/dist/text-msdf.d.ts +70 -0
- package/dist/text-msdf.js +242 -0
- package/dist/text-raster.d.ts +40 -0
- package/dist/text-raster.js +60 -0
- package/dist/text-sdf.d.ts +28 -0
- package/dist/text-sdf.js +119 -0
- package/dist/vector-math.d.ts +19 -0
- package/dist/vector-math.js +37 -0
- package/package.json +80 -4
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { sampleCurve } from "./curve.js";
|
|
2
|
+
import { FrameDrawList2D } from "./frame-draw-list-2d.js";
|
|
3
|
+
import { gridLines } from "./geometry-generators.js";
|
|
4
|
+
import { DEFAULT_FONT_FAMILY } from "./text-font.js";
|
|
5
|
+
import { resolveTextAnchor, DEFAULT_LINE_HEIGHT_SCALE, DEFAULT_FONT_WEIGHT, MAX_FONT_WEIGHT, MIN_FONT_WEIGHT, TEXT_FONT_STYLES, TEXT_FONT_WIDTHS, } from "./text-layout.js";
|
|
6
|
+
export const DEFAULT_COLOR = [1, 1, 1, 1];
|
|
7
|
+
export const DEFAULT_LAYER = 1;
|
|
8
|
+
/** Default block font size in pixels; the world spaces scale it by `DEFAULT_TEXT_PIXEL_SIZE`. */
|
|
9
|
+
export const DEFAULT_TEXT_SIZE = 16;
|
|
10
|
+
/** Default world units per font pixel, i.e. a 0.32 world-unit glyph at the default size. */
|
|
11
|
+
export const DEFAULT_TEXT_PIXEL_SIZE = 0.02;
|
|
12
|
+
/** `world-plane` blocks face +Z until a rotation lays them down, like a reference `Label3D`. */
|
|
13
|
+
export const DEFAULT_TEXT_ROTATION = [0, 0, 0];
|
|
14
|
+
/** reference `LineHeight` default is `RelativeToFont(1.2)`. */
|
|
15
|
+
export const DEFAULT_LINE_HEIGHT_RATIO = DEFAULT_LINE_HEIGHT_SCALE;
|
|
16
|
+
function finite(value, name) {
|
|
17
|
+
if (!Number.isFinite(value)) {
|
|
18
|
+
throw new RangeError(`${name} must be finite`);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function vec3(value, name) {
|
|
23
|
+
if (value.length !== 3) {
|
|
24
|
+
throw new RangeError(`${name} must contain three values`);
|
|
25
|
+
}
|
|
26
|
+
return [finite(value[0], `${name}[0]`), finite(value[1], `${name}[1]`), finite(value[2], `${name}[2]`)];
|
|
27
|
+
}
|
|
28
|
+
function colorValue(value, name) {
|
|
29
|
+
if (value.length !== 4) {
|
|
30
|
+
throw new RangeError(`${name} must contain four values`);
|
|
31
|
+
}
|
|
32
|
+
return [
|
|
33
|
+
finite(value[0], `${name}[0]`),
|
|
34
|
+
finite(value[1], `${name}[1]`),
|
|
35
|
+
finite(value[2], `${name}[2]`),
|
|
36
|
+
finite(value[3], `${name}[3]`),
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
function color(value) {
|
|
40
|
+
return value === undefined ? DEFAULT_COLOR : colorValue(value, "color");
|
|
41
|
+
}
|
|
42
|
+
function layerValue(value, name) {
|
|
43
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
44
|
+
throw new RangeError(`${name} must be a non-negative integer bitmask`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
function layer(value) {
|
|
49
|
+
return value === undefined ? DEFAULT_LAYER : layerValue(value, "layer");
|
|
50
|
+
}
|
|
51
|
+
function sizeModeValue(value, name) {
|
|
52
|
+
if (value !== "screen" && value !== "world") {
|
|
53
|
+
throw new RangeError(`${name} must be "screen" or "world"`);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function jointValue(value, name) {
|
|
58
|
+
if (value !== "none" && value !== "round" && value !== "bevel" && value !== "miter") {
|
|
59
|
+
throw new RangeError(`${name} must be "none", "round", "bevel", or "miter"`);
|
|
60
|
+
}
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
const TEXT_JUSTIFY = ["left", "center", "right", "justified", "start", "end"];
|
|
64
|
+
const TEXT_LINE_BREAK = ["word", "character", "word-or-character", "none"];
|
|
65
|
+
const TEXT_SPACES = ["world-plane", "world-billboard", "screen"];
|
|
66
|
+
function textJustifyValue(value, name) {
|
|
67
|
+
if (!TEXT_JUSTIFY.includes(value)) {
|
|
68
|
+
throw new RangeError(`${name} must be one of ${TEXT_JUSTIFY.join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
function textLineBreakValue(value, name) {
|
|
73
|
+
if (!TEXT_LINE_BREAK.includes(value)) {
|
|
74
|
+
throw new RangeError(`${name} must be one of ${TEXT_LINE_BREAK.join(", ")}`);
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function textSpaceValue(value, name) {
|
|
79
|
+
if (!TEXT_SPACES.includes(value)) {
|
|
80
|
+
throw new RangeError(`${name} must be one of ${TEXT_SPACES.join(", ")}`);
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
function fontWeightValue(value, name) {
|
|
85
|
+
if (!Number.isFinite(value) || value < MIN_FONT_WEIGHT || value > MAX_FONT_WEIGHT) {
|
|
86
|
+
throw new RangeError(`${name} must be between ${MIN_FONT_WEIGHT} and ${MAX_FONT_WEIGHT}`);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function textFontStyleValue(value, name) {
|
|
91
|
+
if (!TEXT_FONT_STYLES.includes(value)) {
|
|
92
|
+
throw new RangeError(`${name} must be one of ${TEXT_FONT_STYLES.join(", ")}`);
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function textFontWidthValue(value, name) {
|
|
97
|
+
if (!TEXT_FONT_WIDTHS.includes(value)) {
|
|
98
|
+
throw new RangeError(`${name} must be one of ${TEXT_FONT_WIDTHS.join(", ")}`);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function textBounds(value, name) {
|
|
103
|
+
if (value.length !== 2)
|
|
104
|
+
throw new RangeError(`${name} must contain two values`);
|
|
105
|
+
if (!(value[0] > 0) || !(value[1] > 0))
|
|
106
|
+
throw new RangeError(`${name} values must be greater than zero`);
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Folds the block defaults into every span so the renderer resolves a style once per span and
|
|
111
|
+
* never has to guess what an omitted field meant.
|
|
112
|
+
*/
|
|
113
|
+
function resolveTextSpans(text, options, defaults, baseFontSize, baseFontFamily, baseWeight, baseStyle, baseWidth) {
|
|
114
|
+
const input = typeof text === "string" ? [{ text }] : text;
|
|
115
|
+
const baseColor = color(options.color ?? defaults.color);
|
|
116
|
+
return input.map((span) => {
|
|
117
|
+
const fontSize = span.fontSize ?? baseFontSize;
|
|
118
|
+
if (!Number.isFinite(fontSize) || fontSize <= 0)
|
|
119
|
+
throw new RangeError("span fontSize must be greater than zero");
|
|
120
|
+
return {
|
|
121
|
+
text: span.text,
|
|
122
|
+
color: span.color === undefined ? baseColor : color(span.color),
|
|
123
|
+
fontSize,
|
|
124
|
+
fontFamily: span.fontFamily ?? baseFontFamily,
|
|
125
|
+
// A span overrides the block's face one axis at a time, like a nested `TextFont` component.
|
|
126
|
+
weight: span.weight === undefined ? baseWeight : fontWeightValue(span.weight, "span weight"),
|
|
127
|
+
style: span.style === undefined ? baseStyle : textFontStyleValue(span.style, "span style"),
|
|
128
|
+
width: span.width === undefined ? baseWidth : textFontWidthValue(span.width, "span width"),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** Validates and resolves style defaults so invalid values fail at construction time. */
|
|
133
|
+
export function resolveFrameDrawListDefaults(defaults = {}) {
|
|
134
|
+
const point = defaults.point ?? {};
|
|
135
|
+
const line = defaults.line ?? {};
|
|
136
|
+
const text = defaults.text ?? {};
|
|
137
|
+
const pointSize = point.size === undefined ? undefined : finite(point.size, "default point size");
|
|
138
|
+
if (pointSize !== undefined && pointSize <= 0) {
|
|
139
|
+
throw new RangeError("default point size must be greater than zero");
|
|
140
|
+
}
|
|
141
|
+
const lineWidth = line.width === undefined ? undefined : finite(line.width, "default line width");
|
|
142
|
+
if (lineWidth !== undefined && lineWidth <= 0) {
|
|
143
|
+
throw new RangeError("default line width must be greater than zero");
|
|
144
|
+
}
|
|
145
|
+
const textSize = text.fontSize === undefined ? undefined : finite(text.fontSize, "default text fontSize");
|
|
146
|
+
if (textSize !== undefined && textSize <= 0) {
|
|
147
|
+
throw new RangeError("default text fontSize must be greater than zero");
|
|
148
|
+
}
|
|
149
|
+
const textPixelSize = text.pixelSize === undefined ? undefined : finite(text.pixelSize, "default text pixelSize");
|
|
150
|
+
if (textPixelSize !== undefined && textPixelSize <= 0) {
|
|
151
|
+
throw new RangeError("default text pixelSize must be greater than zero");
|
|
152
|
+
}
|
|
153
|
+
const textLineHeight = text.lineHeight === undefined ? undefined : finite(text.lineHeight, "default text lineHeight");
|
|
154
|
+
if (textLineHeight !== undefined && textLineHeight <= 0) {
|
|
155
|
+
throw new RangeError("default text lineHeight must be greater than zero");
|
|
156
|
+
}
|
|
157
|
+
const textLineHeightScale = text.lineHeightScale === undefined ? undefined : finite(text.lineHeightScale, "default text lineHeightScale");
|
|
158
|
+
if (textLineHeightScale !== undefined && textLineHeightScale <= 0) {
|
|
159
|
+
throw new RangeError("default text lineHeightScale must be greater than zero");
|
|
160
|
+
}
|
|
161
|
+
if (textLineHeight !== undefined && textLineHeightScale !== undefined) {
|
|
162
|
+
throw new RangeError("default text lineHeight and lineHeightScale are exclusive; pass one or the other");
|
|
163
|
+
}
|
|
164
|
+
const textAnchor = text.anchor;
|
|
165
|
+
if (textAnchor !== undefined)
|
|
166
|
+
resolveTextAnchor(textAnchor);
|
|
167
|
+
const textOutlineWidth = text.outlineWidth === undefined ? undefined : finite(text.outlineWidth, "default text outlineWidth");
|
|
168
|
+
if (textOutlineWidth !== undefined && textOutlineWidth < 0) {
|
|
169
|
+
throw new RangeError("default text outlineWidth must not be negative");
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
point: {
|
|
173
|
+
color: point.color === undefined ? undefined : colorValue(point.color, "default point color"),
|
|
174
|
+
size: pointSize,
|
|
175
|
+
sizeMode: point.sizeMode === undefined ? undefined : sizeModeValue(point.sizeMode, "default point sizeMode"),
|
|
176
|
+
layer: point.layer === undefined ? undefined : layerValue(point.layer, "default point layer"),
|
|
177
|
+
},
|
|
178
|
+
line: {
|
|
179
|
+
color: line.color === undefined ? undefined : colorValue(line.color, "default line color"),
|
|
180
|
+
colorEnd: line.colorEnd === undefined ? undefined : colorValue(line.colorEnd, "default line colorEnd"),
|
|
181
|
+
width: lineWidth,
|
|
182
|
+
widthMode: line.widthMode === undefined ? undefined : sizeModeValue(line.widthMode, "default line widthMode"),
|
|
183
|
+
dash: line.dash,
|
|
184
|
+
normal: line.normal === undefined ? undefined : vec3(line.normal, "default line normal"),
|
|
185
|
+
joint: line.joint === undefined ? undefined : jointValue(line.joint, "default line joint"),
|
|
186
|
+
layer: line.layer === undefined ? undefined : layerValue(line.layer, "default line layer"),
|
|
187
|
+
},
|
|
188
|
+
text: {
|
|
189
|
+
color: text.color === undefined ? undefined : colorValue(text.color, "default text color"),
|
|
190
|
+
outlineWidth: textOutlineWidth,
|
|
191
|
+
outlineColor: text.outlineColor === undefined ? undefined : colorValue(text.outlineColor, "default text outlineColor"),
|
|
192
|
+
fontSize: textSize,
|
|
193
|
+
pixelSize: textPixelSize,
|
|
194
|
+
fontFamily: text.fontFamily,
|
|
195
|
+
weight: text.weight === undefined ? undefined : fontWeightValue(text.weight, "default text weight"),
|
|
196
|
+
style: text.style === undefined ? undefined : textFontStyleValue(text.style, "default text style"),
|
|
197
|
+
width: text.width === undefined ? undefined : textFontWidthValue(text.width, "default text width"),
|
|
198
|
+
lineHeight: textLineHeight,
|
|
199
|
+
lineHeightScale: textLineHeightScale,
|
|
200
|
+
letterSpacing: text.letterSpacing === undefined ? undefined : finite(text.letterSpacing, "default text letterSpacing"),
|
|
201
|
+
justify: text.justify === undefined ? undefined : textJustifyValue(text.justify, "default text justify"),
|
|
202
|
+
lineBreak: text.lineBreak === undefined ? undefined : textLineBreakValue(text.lineBreak, "default text lineBreak"),
|
|
203
|
+
anchor: textAnchor,
|
|
204
|
+
space: text.space === undefined ? undefined : textSpaceValue(text.space, "default text space"),
|
|
205
|
+
rotation: text.rotation === undefined ? undefined : vec3(text.rotation, "default text rotation"),
|
|
206
|
+
source: text.source,
|
|
207
|
+
depthTest: text.depthTest,
|
|
208
|
+
layer: text.layer === undefined ? undefined : layerValue(text.layer, "default text layer"),
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* One frame's draw list: records primitives, then hands them to `GpuDebugDraw3D` as
|
|
214
|
+
* `FrameCommand[]`. This is the `ImDrawList` / `GizmoBuffer` layer; GPU command encoding
|
|
215
|
+
* happens later in the renderer.
|
|
216
|
+
*/
|
|
217
|
+
export class FrameDrawList {
|
|
218
|
+
#points = [];
|
|
219
|
+
#lines = [];
|
|
220
|
+
#joints = [];
|
|
221
|
+
#triangles = [];
|
|
222
|
+
#texts = [];
|
|
223
|
+
#defaults;
|
|
224
|
+
#d2;
|
|
225
|
+
#sealed = false;
|
|
226
|
+
constructor(defaults = {}) {
|
|
227
|
+
this.#defaults = resolveFrameDrawListDefaults(defaults);
|
|
228
|
+
// The 2D layer shares this drawList's resolved point/line styles, so a style set on
|
|
229
|
+
// `DebugDraw3D` cannot apply to one layer and not the other.
|
|
230
|
+
this.#d2 = new FrameDrawList2D({ point: this.#defaults.point, line: this.#defaults.line }, () => this.assertOpen());
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The 2D layer: `Vec2` primitives on the reference-style `Camera2d` spaces described in
|
|
234
|
+
* `camera-2d.ts`. Drawing here records into the same frame and layer mask as the 3D methods.
|
|
235
|
+
*/
|
|
236
|
+
get d2() {
|
|
237
|
+
this.assertOpen();
|
|
238
|
+
return this.#d2;
|
|
239
|
+
}
|
|
240
|
+
point(position, options = {}) {
|
|
241
|
+
this.assertOpen();
|
|
242
|
+
const size = finite(options.size ?? this.#defaults.point.size ?? 1, "size");
|
|
243
|
+
if (size <= 0)
|
|
244
|
+
throw new RangeError("size must be greater than zero");
|
|
245
|
+
this.#points.push({
|
|
246
|
+
kind: "point",
|
|
247
|
+
position: vec3(position, "position"),
|
|
248
|
+
color: color(options.color ?? this.#defaults.point.color),
|
|
249
|
+
size,
|
|
250
|
+
sizeMode: options.sizeMode ?? this.#defaults.point.sizeMode ?? "screen",
|
|
251
|
+
layer: layer(options.layer ?? this.#defaults.point.layer),
|
|
252
|
+
...(options.round === undefined ? {} : { round: options.round }),
|
|
253
|
+
});
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
256
|
+
line(start, end, options = {}) {
|
|
257
|
+
this.assertOpen();
|
|
258
|
+
const resolvedStart = vec3(start, "start");
|
|
259
|
+
const resolvedEnd = vec3(end, "end");
|
|
260
|
+
if (resolvedStart.every((value, index) => value === resolvedEnd[index])) {
|
|
261
|
+
return this;
|
|
262
|
+
}
|
|
263
|
+
const width = finite(options.width ?? this.#defaults.line.width ?? 1, "width");
|
|
264
|
+
if (width <= 0)
|
|
265
|
+
throw new RangeError("width must be greater than zero");
|
|
266
|
+
const dash = options.dash ?? this.#defaults.line.dash;
|
|
267
|
+
const normal = options.normal ?? this.#defaults.line.normal;
|
|
268
|
+
const joint = options.joint ?? this.#defaults.line.joint;
|
|
269
|
+
const command = {
|
|
270
|
+
kind: "line",
|
|
271
|
+
start: resolvedStart,
|
|
272
|
+
end: resolvedEnd,
|
|
273
|
+
color: color(options.color ?? this.#defaults.line.color),
|
|
274
|
+
colorEnd: color(options.colorEnd ?? options.color ?? this.#defaults.line.colorEnd ?? this.#defaults.line.color),
|
|
275
|
+
width,
|
|
276
|
+
widthMode: options.widthMode ?? this.#defaults.line.widthMode ?? "screen",
|
|
277
|
+
layer: layer(options.layer ?? this.#defaults.line.layer),
|
|
278
|
+
...(dash === undefined ? {} : { dash }),
|
|
279
|
+
...(normal === undefined ? {} : { normal: vec3(normal, "normal") }),
|
|
280
|
+
...(joint === undefined ? {} : { joint }),
|
|
281
|
+
};
|
|
282
|
+
this.#lines.push(command);
|
|
283
|
+
return this;
|
|
284
|
+
}
|
|
285
|
+
ray(origin, direction, options = {}) {
|
|
286
|
+
return this.line(origin, [origin[0] + direction[0], origin[1] + direction[1], origin[2] + direction[2]], options);
|
|
287
|
+
}
|
|
288
|
+
arrow(origin, direction, options = {}) {
|
|
289
|
+
const end = [origin[0] + direction[0], origin[1] + direction[1], origin[2] + direction[2]];
|
|
290
|
+
this.line(origin, end, options);
|
|
291
|
+
const length = Math.hypot(direction[0], direction[1], direction[2]) || 1;
|
|
292
|
+
const headLength = options.headLength ?? Math.min(0.18, length * 0.25);
|
|
293
|
+
const headAngle = options.headAngle ?? Math.PI / 6;
|
|
294
|
+
const forward = [direction[0] / length, direction[1] / length, direction[2] / length];
|
|
295
|
+
const reference = Math.abs(forward[1]) < 0.9 ? [0, 1, 0] : [1, 0, 0];
|
|
296
|
+
const side = [forward[1] * reference[2] - forward[2] * reference[1], forward[2] * reference[0] - forward[0] * reference[2], forward[0] * reference[1] - forward[1] * reference[0]];
|
|
297
|
+
const sideLength = Math.hypot(side[0], side[1], side[2]) || 1;
|
|
298
|
+
const unitSide = [side[0] / sideLength, side[1] / sideLength, side[2] / sideLength];
|
|
299
|
+
const back = Math.cos(headAngle) * headLength;
|
|
300
|
+
const spread = Math.sin(headAngle) * headLength;
|
|
301
|
+
const left = [end[0] - forward[0] * back + unitSide[0] * spread, end[1] - forward[1] * back + unitSide[1] * spread, end[2] - forward[2] * back + unitSide[2] * spread];
|
|
302
|
+
const right = [end[0] - forward[0] * back - unitSide[0] * spread, end[1] - forward[1] * back - unitSide[1] * spread, end[2] - forward[2] * back - unitSide[2] * spread];
|
|
303
|
+
this.line(end, left, options);
|
|
304
|
+
this.line(end, right, options);
|
|
305
|
+
return this;
|
|
306
|
+
}
|
|
307
|
+
axes(origin, length = 1, width = 2) {
|
|
308
|
+
// Reference `draw_axes` emits three unadorned axis lines (no arrowheads).
|
|
309
|
+
this.line(origin, [origin[0] + length, origin[1], origin[2]], { color: [1, 0, 0, 1], width });
|
|
310
|
+
this.line(origin, [origin[0], origin[1] + length, origin[2]], { color: [0, 1, 0, 1], width });
|
|
311
|
+
this.line(origin, [origin[0], origin[1], origin[2] + length], { color: [0, 0, 1, 1], width });
|
|
312
|
+
return this;
|
|
313
|
+
}
|
|
314
|
+
lineStrip(points, options = {}, closed = false) {
|
|
315
|
+
this.assertOpen();
|
|
316
|
+
// Dash distance must accumulate across the whole path, otherwise each segment
|
|
317
|
+
// restarts the pattern and closed shapes render as a nearly continuous line.
|
|
318
|
+
const dash = options.dash ?? this.#defaults.line.dash;
|
|
319
|
+
// Per-vertex normals mirror the reference `*_n` entry points: each segment is emitted
|
|
320
|
+
// with its start vertex's normal instead of one normal for the whole path.
|
|
321
|
+
const normals = options.normals;
|
|
322
|
+
if (normals !== undefined) {
|
|
323
|
+
// Reference `draw_line_strip_n` accepts one normal per segment, `draw_line_loop_n` one
|
|
324
|
+
// per vertex; the loop's closing segment reuses the last vertex's normal.
|
|
325
|
+
const required = closed ? points.length : Math.max(0, points.length - 1);
|
|
326
|
+
if (normals.length < required) {
|
|
327
|
+
throw new RangeError(`normals must have at least ${required} entries for this path`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const { normals: _normals, ...sharedOptions } = options;
|
|
331
|
+
void _normals;
|
|
332
|
+
const vertexDistances = [0];
|
|
333
|
+
let traveled = 0;
|
|
334
|
+
const segment = (a, b, index) => {
|
|
335
|
+
const normal = normals === undefined ? undefined : normals[index];
|
|
336
|
+
const segmentOptions = {
|
|
337
|
+
...sharedOptions,
|
|
338
|
+
...(dash === undefined ? {} : { dash: [dash[0], dash[1], dash[2] + traveled] }),
|
|
339
|
+
...(normal === undefined ? {} : { normal }),
|
|
340
|
+
};
|
|
341
|
+
this.line(a, b, segmentOptions);
|
|
342
|
+
traveled += Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
|
|
343
|
+
vertexDistances.push(traveled);
|
|
344
|
+
};
|
|
345
|
+
for (let i = 0; i + 1 < points.length; i++)
|
|
346
|
+
segment(points[i], points[i + 1], i);
|
|
347
|
+
if (closed && points.length > 1)
|
|
348
|
+
segment(points[points.length - 1], points[0], points.length - 1);
|
|
349
|
+
const joint = options.joint ?? this.#defaults.line.joint;
|
|
350
|
+
// Reference `_emit_line_strip` / `_emit_line_loop`: an open strip only joins its
|
|
351
|
+
// interior vertices, while a loop also joins the two wrap-around seam vertices.
|
|
352
|
+
if (joint === "round" || joint === "bevel" || joint === "miter") {
|
|
353
|
+
const count = points.length;
|
|
354
|
+
const first = closed ? 0 : 1;
|
|
355
|
+
const last = closed ? (count > 2 ? count : 0) : count - 1;
|
|
356
|
+
for (let i = first; i < last; i++) {
|
|
357
|
+
const previousIndex = (i - 1 + count) % count;
|
|
358
|
+
this.#jointCommand(points[previousIndex], points[i], points[(i + 1) % count], joint, sharedOptions, dash, vertexDistances[i], normals === undefined ? undefined : [normals[previousIndex], normals[i]]);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Emits one joint instance for a path vertex. The reference skips degenerate wedges
|
|
365
|
+
* where the center coincides with a neighbour (`emit_joint`), and reuses the resolved
|
|
366
|
+
* line width/color/layer so segment and joint strokes cannot drift apart.
|
|
367
|
+
*/
|
|
368
|
+
#jointCommand(previous, center, next, joint, options, dash, distance, normals) {
|
|
369
|
+
const resolvedPrevious = vec3(previous, "previous");
|
|
370
|
+
const resolvedCenter = vec3(center, "center");
|
|
371
|
+
const resolvedNext = vec3(next, "next");
|
|
372
|
+
const same = (a, b) => a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
373
|
+
if (same(resolvedPrevious, resolvedCenter) || same(resolvedCenter, resolvedNext))
|
|
374
|
+
return;
|
|
375
|
+
const width = finite(options.width ?? this.#defaults.line.width ?? 1, "width");
|
|
376
|
+
if (width <= 0)
|
|
377
|
+
throw new RangeError("width must be greater than zero");
|
|
378
|
+
this.#joints.push({
|
|
379
|
+
kind: "joint",
|
|
380
|
+
previous: resolvedPrevious,
|
|
381
|
+
center: resolvedCenter,
|
|
382
|
+
next: resolvedNext,
|
|
383
|
+
color: color(options.color ?? this.#defaults.line.color),
|
|
384
|
+
width,
|
|
385
|
+
widthMode: options.widthMode ?? this.#defaults.line.widthMode ?? "screen",
|
|
386
|
+
joint,
|
|
387
|
+
layer: layer(options.layer ?? this.#defaults.line.layer),
|
|
388
|
+
...(dash === undefined ? {} : { dash: [dash[0], dash[1], dash[2] + distance] }),
|
|
389
|
+
...(normals === undefined ? {} : { previousNormal: vec3(normals[0], "previousNormal"), nextNormal: vec3(normals[1], "nextNormal") }),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
polyline(points, options = {}, closed = false) {
|
|
393
|
+
return this.lineStrip(points, options, closed);
|
|
394
|
+
}
|
|
395
|
+
triangle(a, b, c, options = {}) {
|
|
396
|
+
this.assertOpen();
|
|
397
|
+
this.#triangles.push({
|
|
398
|
+
kind: "triangle",
|
|
399
|
+
a: vec3(a, "a"),
|
|
400
|
+
b: vec3(b, "b"),
|
|
401
|
+
c: vec3(c, "c"),
|
|
402
|
+
color: color(options.color),
|
|
403
|
+
layer: layer(options.layer),
|
|
404
|
+
});
|
|
405
|
+
return this;
|
|
406
|
+
}
|
|
407
|
+
mesh(mesh, options = {}) {
|
|
408
|
+
const shared = {
|
|
409
|
+
...(options.color === undefined ? {} : { color: options.color }),
|
|
410
|
+
...(options.layer === undefined ? {} : { layer: options.layer }),
|
|
411
|
+
};
|
|
412
|
+
if (options.solid) {
|
|
413
|
+
for (const [a, b, c] of mesh.triangles)
|
|
414
|
+
this.triangle(a, b, c, shared);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
for (const [a, b] of mesh.segments)
|
|
418
|
+
this.line(a, b, shared);
|
|
419
|
+
}
|
|
420
|
+
return this;
|
|
421
|
+
}
|
|
422
|
+
curve(curve, options = {}) {
|
|
423
|
+
const points = sampleCurve(curve, options.tolerance, options.transform);
|
|
424
|
+
const { tolerance: _tolerance, transform: _transform, ...lineOptions } = options;
|
|
425
|
+
void _tolerance;
|
|
426
|
+
void _transform;
|
|
427
|
+
return this.lineStrip(points, lineOptions, curve.closed ?? false);
|
|
428
|
+
}
|
|
429
|
+
grid(center, cells, spacing, options = {}) {
|
|
430
|
+
const { rotation, skew, outer, ...lineOptions } = options;
|
|
431
|
+
for (const [a, b] of gridLines(cells, spacing, center, rotation, skew, outer))
|
|
432
|
+
this.line(a, b, lineOptions);
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Records one text block. `text` is either the block's string or a list of spans, mirroring
|
|
437
|
+
* reference `Text2d`/`Text` root plus its child `TextSpan`s; a span's own style overrides the
|
|
438
|
+
* block default. Layout happens at draw time, because only the renderer knows where the
|
|
439
|
+
* glyph atlas placed each character.
|
|
440
|
+
*/
|
|
441
|
+
text(position, text, options = {}) {
|
|
442
|
+
this.assertOpen();
|
|
443
|
+
const defaults = this.#defaults.text;
|
|
444
|
+
const fontSize = finite(options.fontSize ?? defaults.fontSize ?? DEFAULT_TEXT_SIZE, "fontSize");
|
|
445
|
+
if (fontSize <= 0)
|
|
446
|
+
throw new RangeError("fontSize must be greater than zero");
|
|
447
|
+
const pixelSize = finite(options.pixelSize ?? defaults.pixelSize ?? DEFAULT_TEXT_PIXEL_SIZE, "pixelSize");
|
|
448
|
+
if (pixelSize <= 0)
|
|
449
|
+
throw new RangeError("pixelSize must be greater than zero");
|
|
450
|
+
const lineHeightOption = options.lineHeight ?? defaults.lineHeight;
|
|
451
|
+
const lineHeightScaleOption = options.lineHeightScale ?? defaults.lineHeightScale;
|
|
452
|
+
if (lineHeightOption !== undefined && lineHeightScaleOption !== undefined) {
|
|
453
|
+
throw new RangeError("lineHeight and lineHeightScale are exclusive; pass one or the other");
|
|
454
|
+
}
|
|
455
|
+
// A relative line height stays relative all the way to layout, where each line resolves it
|
|
456
|
+
// against that line's own largest span; only an explicit one is absolute.
|
|
457
|
+
const lineHeightScale = lineHeightOption === undefined
|
|
458
|
+
? finite(lineHeightScaleOption ?? DEFAULT_LINE_HEIGHT_RATIO, "lineHeightScale")
|
|
459
|
+
: undefined;
|
|
460
|
+
if (lineHeightScale !== undefined && lineHeightScale <= 0)
|
|
461
|
+
throw new RangeError("lineHeightScale must be greater than zero");
|
|
462
|
+
const lineHeight = lineHeightScale === undefined ? finite(lineHeightOption, "lineHeight") : fontSize * lineHeightScale;
|
|
463
|
+
if (lineHeight <= 0)
|
|
464
|
+
throw new RangeError("lineHeight must be greater than zero");
|
|
465
|
+
const letterSpacing = finite(options.letterSpacing ?? defaults.letterSpacing ?? 0, "letterSpacing");
|
|
466
|
+
const fontFamily = options.fontFamily ?? defaults.fontFamily ?? DEFAULT_FONT_FAMILY;
|
|
467
|
+
const weight = fontWeightValue(options.weight ?? defaults.weight ?? DEFAULT_FONT_WEIGHT, "weight");
|
|
468
|
+
const style = textFontStyleValue(options.style ?? defaults.style ?? "normal", "style");
|
|
469
|
+
const width = textFontWidthValue(options.width ?? defaults.width ?? "normal", "width");
|
|
470
|
+
const justify = options.justify ?? defaults.justify ?? "left";
|
|
471
|
+
const lineBreak = options.lineBreak ?? defaults.lineBreak ?? "word";
|
|
472
|
+
const anchor = options.anchor ?? defaults.anchor ?? "center";
|
|
473
|
+
const space = options.space ?? defaults.space ?? "world-plane";
|
|
474
|
+
const rotation = vec3(options.rotation ?? defaults.rotation ?? DEFAULT_TEXT_ROTATION, "rotation");
|
|
475
|
+
const source = options.source ?? defaults.source;
|
|
476
|
+
const bounds = options.bounds;
|
|
477
|
+
if (bounds !== undefined)
|
|
478
|
+
textBounds(bounds, "bounds");
|
|
479
|
+
const outlineWidth = finite(options.outlineWidth ?? defaults.outlineWidth ?? 0, "outlineWidth");
|
|
480
|
+
if (outlineWidth < 0)
|
|
481
|
+
throw new RangeError("outlineWidth must not be negative");
|
|
482
|
+
const outlineColor = color(options.outlineColor ?? defaults.outlineColor ?? options.color ?? defaults.color);
|
|
483
|
+
// Validates the anchor name and the enumerated fields with the same messages defaults use.
|
|
484
|
+
resolveTextAnchor(anchor);
|
|
485
|
+
textJustifyValue(justify, "justify");
|
|
486
|
+
textLineBreakValue(lineBreak, "lineBreak");
|
|
487
|
+
textSpaceValue(space, "space");
|
|
488
|
+
this.#texts.push({
|
|
489
|
+
kind: "text",
|
|
490
|
+
position: vec3(position, "position"),
|
|
491
|
+
spans: resolveTextSpans(text, options, defaults, fontSize, fontFamily, weight, style, width),
|
|
492
|
+
color: color(options.color ?? defaults.color),
|
|
493
|
+
outlineWidth,
|
|
494
|
+
outlineColor,
|
|
495
|
+
fontSize,
|
|
496
|
+
pixelSize,
|
|
497
|
+
fontFamily,
|
|
498
|
+
weight,
|
|
499
|
+
style,
|
|
500
|
+
width,
|
|
501
|
+
lineHeight,
|
|
502
|
+
...(lineHeightScale === undefined ? {} : { lineHeightScale }),
|
|
503
|
+
letterSpacing,
|
|
504
|
+
justify,
|
|
505
|
+
lineBreak,
|
|
506
|
+
anchor,
|
|
507
|
+
space,
|
|
508
|
+
rotation,
|
|
509
|
+
...(source === undefined ? {} : { source }),
|
|
510
|
+
depthTest: options.depthTest ?? defaults.depthTest ?? true,
|
|
511
|
+
layer: layer(options.layer ?? defaults.layer),
|
|
512
|
+
...(bounds === undefined ? {} : { bounds }),
|
|
513
|
+
});
|
|
514
|
+
return this;
|
|
515
|
+
}
|
|
516
|
+
get points() {
|
|
517
|
+
return this.#points;
|
|
518
|
+
}
|
|
519
|
+
get lines() {
|
|
520
|
+
return this.#lines;
|
|
521
|
+
}
|
|
522
|
+
get joints() {
|
|
523
|
+
return this.#joints;
|
|
524
|
+
}
|
|
525
|
+
get texts() {
|
|
526
|
+
return this.#texts;
|
|
527
|
+
}
|
|
528
|
+
commands(visibleLayers = 0xffff_ffff) {
|
|
529
|
+
this.assertOpen();
|
|
530
|
+
if (!Number.isInteger(visibleLayers) || visibleLayers < 0) {
|
|
531
|
+
throw new RangeError("visibleLayers must be a non-negative integer bitmask");
|
|
532
|
+
}
|
|
533
|
+
// Text draws last: it is the only alpha-blended primitive that is not depth-sorted against
|
|
534
|
+
// its own kind, so keeping it at the end preserves the reference's reading order. The 2D
|
|
535
|
+
// commands follow the 3D ones because the renderer draws them as an overlay.
|
|
536
|
+
return [
|
|
537
|
+
...this.#points,
|
|
538
|
+
...this.#lines,
|
|
539
|
+
...this.#joints,
|
|
540
|
+
...this.#triangles,
|
|
541
|
+
...this.#texts,
|
|
542
|
+
...this.#d2.points,
|
|
543
|
+
...this.#d2.lines,
|
|
544
|
+
...this.#d2.joints,
|
|
545
|
+
...this.#d2.shapes,
|
|
546
|
+
].filter((command) => (command.layer & visibleLayers) !== 0);
|
|
547
|
+
}
|
|
548
|
+
seal() {
|
|
549
|
+
this.assertOpen();
|
|
550
|
+
this.#sealed = true;
|
|
551
|
+
}
|
|
552
|
+
assertOpen() {
|
|
553
|
+
if (this.#sealed)
|
|
554
|
+
throw new Error("frame drawList is sealed");
|
|
555
|
+
}
|
|
556
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Vec3 } from "./frame-draw-list.js";
|
|
2
|
+
import { type CircleBasis } from "./vector-math.js";
|
|
3
|
+
export type Triangle = readonly [Vec3, Vec3, Vec3];
|
|
4
|
+
export type Segment = readonly [Vec3, Vec3];
|
|
5
|
+
export type MeshData = {
|
|
6
|
+
readonly triangles: readonly Triangle[];
|
|
7
|
+
readonly segments: readonly Segment[];
|
|
8
|
+
};
|
|
9
|
+
export declare function rect(size: readonly [number, number], center?: Vec3, rotation?: readonly [number, number, number]): MeshData;
|
|
10
|
+
export declare function box(size: Vec3, center?: Vec3, rotation?: readonly [number, number, number]): MeshData;
|
|
11
|
+
export declare function disc(radius: number, segments?: number, center?: Vec3, rotation?: readonly [number, number, number]): MeshData;
|
|
12
|
+
export declare function arc(radius: number, angle: number, segments?: number, center?: Vec3, rotation?: readonly [number, number, number]): MeshData;
|
|
13
|
+
export declare function cylinder(radius: number, halfHeight: number, segments?: number, center?: Vec3, rotation?: readonly [number, number, number]): MeshData;
|
|
14
|
+
export declare function cone(radius: number, height: number, segments?: number, center?: Vec3, rotation?: readonly [number, number, number]): MeshData;
|
|
15
|
+
export declare function sphere(radius: number, segments?: number, rings?: number, center?: Vec3, rotation?: readonly [number, number, number]): MeshData;
|
|
16
|
+
export declare function capsule(radius: number, halfHeight: number, segments?: number, rings?: number, center?: Vec3, rotation?: readonly [number, number, number]): MeshData;
|
|
17
|
+
/**
|
|
18
|
+
* Grid line segments, ported from `_emit_grid_lines` in `debug_draw_3d.gd`.
|
|
19
|
+
*
|
|
20
|
+
* Alignment contract, which the previous implementation got backwards:
|
|
21
|
+
* - `outer` selects which boundary lines are **kept**: without `x`, the outermost line
|
|
22
|
+
* along X is dropped (`start = 1`, `end = cellCount`); with `x` the full range
|
|
23
|
+
* (`0 .. cellCount + 1`) is emitted. So `"none"` keeps only interior lines, and `"xz"`
|
|
24
|
+
* keeps every line including both boundaries.
|
|
25
|
+
* - `skew` slants the lines themselves (`dx.z = sx * tan(skew.y)`,
|
|
26
|
+
* `dz.x = sz * tan(skew.x)`), not just an offset.
|
|
27
|
+
*/
|
|
28
|
+
/** Closed paths the reference draws with `draw_line_loop`, plus `draw_line_list` segments. */
|
|
29
|
+
export type ShapeWire = {
|
|
30
|
+
readonly loops: readonly (readonly Vec3[])[];
|
|
31
|
+
readonly lines: readonly Segment[];
|
|
32
|
+
};
|
|
33
|
+
/** `_get_cone_base_rotation`: `Basis(width_axis, axis.cross(width_axis), axis)`. */
|
|
34
|
+
export declare function coneBaseBasis(basis: CircleBasis): CircleBasis;
|
|
35
|
+
/** `_get_circle_points`: `center + (basis.x * cos + basis.y * sin) * radius`. */
|
|
36
|
+
export declare function circlePoints(center: Vec3, radius: number, segments: number, basis: CircleBasis): Vec3[];
|
|
37
|
+
/**
|
|
38
|
+
* `_get_circle_normals`: outward normals of a circle, sampled half a segment ahead of each
|
|
39
|
+
* point so every segment gets the radial direction at its own midpoint.
|
|
40
|
+
*/
|
|
41
|
+
export declare function circleNormals(segments: number, basis: CircleBasis): Vec3[];
|
|
42
|
+
/** `_get_sphere_ring_normals`: each normal points from the sphere centre through the midpoint. */
|
|
43
|
+
export declare function sphereRingNormals(sphereCenter: Vec3, points: readonly Vec3[]): Vec3[];
|
|
44
|
+
/**
|
|
45
|
+
* `_get_capsule_side_outline_normals`: project each segment midpoint onto the capsule axis
|
|
46
|
+
* and use the direction from that axis point as the normal.
|
|
47
|
+
*
|
|
48
|
+
* Applied to a cap ring this reproduces `_get_circle_normals` exactly, because the ring
|
|
49
|
+
* midpoints project onto the cap centre and the resulting directions are the ring radials.
|
|
50
|
+
*/
|
|
51
|
+
export declare function capsuleOutlineNormals(bottomCenter: Vec3, topCenter: Vec3, axis: Vec3, points: readonly Vec3[]): Vec3[];
|
|
52
|
+
/**
|
|
53
|
+
* `_get_arc_normals`: radial normals at each segment midpoint of an arc sweep. An arc is a
|
|
54
|
+
* strip, so it takes one normal per segment rather than per vertex.
|
|
55
|
+
*/
|
|
56
|
+
export declare function arcNormals(angle: number, segments: number, basis: CircleBasis): Vec3[];
|
|
57
|
+
/**
|
|
58
|
+
* `_get_rect_normals`: one normal per corner, in the `_get_rect_points` order `rectLoop` uses
|
|
59
|
+
* (`-w-h`, `+w-h`, `+w+h`, `-w+h`).
|
|
60
|
+
*/
|
|
61
|
+
export declare function rectNormals(basis: CircleBasis): Vec3[];
|
|
62
|
+
/**
|
|
63
|
+
* `_get_box_line_normals`, derived from the edges themselves rather than from a fixed table.
|
|
64
|
+
*
|
|
65
|
+
* A box edge runs along one basis axis and joins the two faces whose normals are the other two
|
|
66
|
+
* axes, so its normal is the sum of those two unit axes with the signs of the edge's position.
|
|
67
|
+
* Deriving it keeps the normals aligned with whatever order the mesh generator emits edges in,
|
|
68
|
+
* instead of depending on the reference's own point order.
|
|
69
|
+
*/
|
|
70
|
+
export declare function boxEdgeNormals(segments: readonly Segment[], center: Vec3, basis: CircleBasis): Vec3[];
|
|
71
|
+
/** `_get_cone_side_line_normals`: one normal per side line, tilted along the cone slope. */
|
|
72
|
+
export declare function coneSideLineNormals(radius: number, height: number, sideSegmentCount: number, basis: CircleBasis): Vec3[];
|
|
73
|
+
/** `_get_cylinder_side_line_normals`: one radial normal per wall line. */
|
|
74
|
+
export declare function cylinderSideLineNormals(radius: number, halfHeight: number, sideSegmentCount: number, basis: CircleBasis): Vec3[];
|
|
75
|
+
/**
|
|
76
|
+
* `_emit_sphere_axis_rings`: `y_axis_segment_count` latitude rings plus
|
|
77
|
+
* `z_axis_segment_count` meridian great circles, de-duplicated by ring key.
|
|
78
|
+
*
|
|
79
|
+
* - Latitude rings sit at `radius * (-1 + 2i/n)` for `i in 1..n-1` with ring radius
|
|
80
|
+
* `sqrt(r^2 - offset^2)`, so `n < 2` draws none and `n = 2` draws the equator only.
|
|
81
|
+
* - Great circles all have radius `radius` and advance by `PI / z` around `basis.y`, so
|
|
82
|
+
* `z < 1` draws none. A count of `z` yields `ceil(z / 2)` distinct circles, because the
|
|
83
|
+
* ring key folds opposite normals together.
|
|
84
|
+
*/
|
|
85
|
+
export declare function sphereWire(radius: number, segments: number, yAxisSegmentCount: number, zAxisSegmentCount: number, center: Vec3, basis: CircleBasis): ShapeWire;
|
|
86
|
+
/**
|
|
87
|
+
* `_emit_capsule`: bottom/top cylinder rings plus `side_segment_count` closed side
|
|
88
|
+
* outlines. The reference has no solid capsule variant, so this is the whole wireframe.
|
|
89
|
+
*/
|
|
90
|
+
export declare function capsuleWire(radius: number, halfHeight: number, segments: number, sideSegmentCount: number, center: Vec3, basis: CircleBasis): ShapeWire;
|
|
91
|
+
/**
|
|
92
|
+
* `_get_cone_base_points` + `_get_cone_side_line_points`: the base ring is a loop and
|
|
93
|
+
* the side lines are `apex -> base point` pairs emitted as a line list.
|
|
94
|
+
*/
|
|
95
|
+
export declare function coneWire(radius: number, height: number, segments: number, sideSegmentCount: number, center: Vec3, basis: CircleBasis): ShapeWire;
|
|
96
|
+
/**
|
|
97
|
+
* `_get_cylinder_ring_points` + `_get_cylinder_side_line_points`: two rings and
|
|
98
|
+
* `side_segment_count` vertical wall segments.
|
|
99
|
+
*/
|
|
100
|
+
export declare function cylinderWire(radius: number, halfHeight: number, segments: number, sideSegmentCount: number, center: Vec3, basis: CircleBasis): ShapeWire;
|
|
101
|
+
export declare function gridLines(cells: readonly [number, number], spacing: readonly [number, number], center?: Vec3, rotation?: readonly [number, number, number], skew?: readonly [number, number], outer?: "none" | "x" | "z" | "xz"): Segment[];
|