@weasel-js/kernel3d 1.16.0 → 1.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,8 +4,9 @@ Poses, an orbit camera, ray picking and screen-projected chrome geometry for
4
4
  weasel, over `@weasel-js/core`'s scene graph and dispatcher.
5
5
 
6
6
  **It hosts a renderer rather than owning one.** A consumer brings its own — a
7
- bespoke WebGL pass, three.js, anything — and the kernel hands it poses. Nothing
8
- here touches WebGL.
7
+ bespoke WebGL pass, three.js, anything — and the kernel hands it poses. The one
8
+ thing here that touches WebGL is the line pass (below), which that renderer
9
+ calls into its own context.
9
10
 
10
11
  ```ts
11
12
  import { createScene } from '@weasel-js/core';
@@ -110,6 +111,41 @@ makes a pane a target, unprojecting onto the same ground, and `cameraOfAim3d`
110
111
  gives back the camera a dragged aim stands for — what an `AimSource`'s `setAim`
111
112
  writes.
112
113
 
114
+ ## Lines
115
+
116
+ A stroke has no mesh a renderer can draw, so the kernel ships a pass for it.
117
+ `packLines` turns polylines into vertices, and `createLinePass(gl)` draws them
118
+ into a WebGL2 context the host owns, between its own draws:
119
+
120
+ ```ts
121
+ const pass = createLinePass(gl);
122
+ pass.draw(
123
+ packLines([{ runs: [{ points }], color: [1, 0.4, 0.2, 1], width: 4, trim: { end: 0.6 }, model }]),
124
+ cameraViewProjection(camera, width / height),
125
+ { width, height }, // the viewport in CSS pixels
126
+ );
127
+ ```
128
+
129
+ - **Widths are pixels.** Each line is widened after projection, so its shaft and
130
+ head are the same size near the camera and far from it. Corners miter along
131
+ the bisector, up to a limit past which a hairpin keeps a notch.
132
+ - **A trim is a `StrokeTrim`, measured in the line's own space** before `model`
133
+ places it, through geom's length table and stroke's `resolveTrim`, as a 2D
134
+ stroke's is. It is applied when packing, so an animated trim repacks.
135
+ - **A head caps each open run's end**: `HEAD` by default, a `HeadShape` to set
136
+ one, `null` for none. It is capped at a share of the line's length on screen,
137
+ and the shaft narrows by what the cap took. It stands back from the tip along
138
+ its own segment's direction on screen, so a head longer than its segment
139
+ stays on that segment's line. A closed subpath, or a run cut from one, has no
140
+ head.
141
+ - **A segment with an end behind the eye is dropped**, not drawn across the
142
+ screen.
143
+
144
+ Depth is tested against the bound buffer by default and never written; pass
145
+ `{ depthTest: false }` to draw over everything. The pass puts back the GL state
146
+ it changes. A three.js host calls it after `renderer.render`, with
147
+ `renderer.getContext()`, and then `renderer.resetState()`.
148
+
113
149
  ## Math
114
150
 
115
151
  Vectors, quaternions, 4×4 matrices and ray intersection live in
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Vec3, Mat4, Quat, Aabb, Ray } from '@weasel-js/geom/3d';
2
- import { PoseComposition, SceneNode, Scene, NodeId, SnapSource, SnapOutcomeStore, AreaSelectDep, NodeAtPointDep, PoseDescriptor, SnapDep, InsertExtras, InsertDep, ViewAim, AimSpace, SnapMark, OngoingHandle, Action, Tool } from '@weasel-js/core';
2
+ import { PoseComposition, SceneNode, Scene, SnapSource, SnapOutcomeStore, AreaSelectDep, NodeAtPointDep, PoseDescriptor, SnapDep, InsertExtras, InsertDep, ViewAim, AimSpace, SnapMark } from '@weasel-js/core/engine';
3
+ import { NodeId, OngoingHandle, Action, Tool } from '@weasel-js/routing';
3
4
  import { KitSettingsStore } from '@weasel-js/react';
5
+ import { StrokeTrim } from '@weasel-js/paint';
4
6
  import * as _weasel_js_prefs from '@weasel-js/prefs';
5
7
 
6
8
  /**
@@ -464,6 +466,145 @@ declare function collectOverlayBoxes(handles: Iterable<OngoingHandle>, origin: {
464
466
  y: number;
465
467
  }): ChromeBox[];
466
468
 
469
+ /**
470
+ * The numbers an arrowhead is cut from, and how a line's shaft and head share
471
+ * its length on screen. The line pass's vertex shader repeats {@link layoutLine}
472
+ * in GLSL; the two must agree.
473
+ */
474
+ /**
475
+ * An arrowhead, set by the shaft it caps: a line is set by its width alone and
476
+ * its head follows, so a wide line is still an arrow rather than a band with a
477
+ * small head poking out of it.
478
+ */
479
+ interface HeadShape {
480
+ /** The head's length, in multiples of half the shaft's width. */
481
+ lenRatio: number;
482
+ /** The most of the line's on-screen length the head may take. */
483
+ maxShare: number;
484
+ /** Half the head's base as a share of its length. The cap shortens a head; it never reshapes it. */
485
+ aspect: number;
486
+ }
487
+ /** A 16 px head 14 px across on a 4 px shaft, taking at most half the line. */
488
+ declare const HEAD: HeadShape;
489
+ /**
490
+ * How far the shaft runs on under the head, as a share of the head's length:
491
+ * enough to close the seam between them, little enough that a curving shaft
492
+ * stays inside the triangle.
493
+ */
494
+ declare const HEAD_OVERDRAW = 0.1;
495
+ /** The head a shaft carries, in pixels, and the shaft that goes under it. */
496
+ interface HeadSize {
497
+ /** The head's length. */
498
+ headLen: number;
499
+ /** Half the head's base. */
500
+ headHalf: number;
501
+ /** Half the shaft's width: narrowed by whatever the cap took off the head. */
502
+ shaftHalf: number;
503
+ }
504
+ /**
505
+ * The head `shaftHalf` carries on a line `linePx` long on screen. The head is
506
+ * capped at `head.maxShare` of the line, and the shaft narrows by the share
507
+ * the cap took, so a short line is the same arrow drawn smaller rather than a
508
+ * full-width band with a stub on the end. Left out, `linePx` caps nothing.
509
+ */
510
+ declare function headFor(shaftHalf: number, linePx?: number, head?: HeadShape): HeadSize;
511
+ /** {@link HeadSize}, plus where along the line its shaft stops. */
512
+ interface LineLayout extends HeadSize {
513
+ /** Where the shaft ends, in the line's own length units. */
514
+ shaftEnd: number;
515
+ }
516
+ /**
517
+ * How a line `length` long in its own units, drawn at `pxPerUnit` pixels per
518
+ * unit near its tip, splits between shaft and head. A line with no head
519
+ * (`head` null) or no known scale runs its shaft to the end.
520
+ */
521
+ declare function layoutLine(shaftHalf: number, length: number, pxPerUnit: number, head: HeadShape | null): LineLayout;
522
+
523
+ /**
524
+ * Lines as the line pass's vertices. Trimming happens here, on the CPU and in
525
+ * the line's own space; widths and the head are pixels, laid out by the shader
526
+ * once the camera is known.
527
+ */
528
+
529
+ /** One subpath of a {@link Line3}: points joined by straight segments. */
530
+ interface LineRun3 {
531
+ points: readonly Vec3[];
532
+ /** Whether a closing edge runs from the last point back to the first. */
533
+ closed?: boolean;
534
+ }
535
+ /** A polyline drawn a fixed number of pixels wide, whatever its depth. */
536
+ interface Line3 {
537
+ /** Subpaths in pen order, which is the order a trim runs. */
538
+ runs: readonly LineRun3[];
539
+ /** Straight (not premultiplied) RGBA, each 0–1. */
540
+ color: readonly [number, number, number, number];
541
+ /** The shaft's width in CSS pixels. Default 4. */
542
+ width?: number;
543
+ /** The head at each open run's end; null for none. Default {@link HEAD}. */
544
+ head?: HeadShape | null;
545
+ /** The stretch drawn, measured in the line's own space. */
546
+ trim?: StrokeTrim;
547
+ /** From the line's own space to the world. Default identity. */
548
+ model?: Mat4;
549
+ }
550
+ /** A visible stretch of a line: what the pass draws as one shaft and at most one head. */
551
+ interface VisibleRun3 {
552
+ /** In the line's own space. */
553
+ points: Vec3[];
554
+ closed: boolean;
555
+ headed: boolean;
556
+ }
557
+ /** Floats per vertex; see {@link packLines} for the layout. */
558
+ declare const LINE_FLOATS = 29;
559
+ /**
560
+ * The stretches of `line` its trim leaves, one per subpath the trim reaches.
561
+ * A closed subpath the trim covers whole stays closed; one it cuts is open and,
562
+ * as a closed subpath takes no marker, carries no head.
563
+ */
564
+ declare function visibleRuns(line: Line3): VisibleRun3[];
565
+ /**
566
+ * `lines` as the line pass draws them: six vertices a segment, three more for
567
+ * a head, `LINE_FLOATS` floats each, laid out as
568
+ *
569
+ * | floats | holds |
570
+ * |---|---|
571
+ * | 0–2, 3–5 | the segment's two ends, in the world |
572
+ * | 6–8 | the point past this vertex's own end, so the shaft can miter |
573
+ * | 9, 10 | role and side |
574
+ * | 11–13 | the segment's start and end along its run, and the run's length, in the line's own units |
575
+ * | 14–17 | half the shaft's width in pixels, then the head's `lenRatio`, `maxShare` and `aspect` (`lenRatio` 0: no head) |
576
+ * | 18–20, 21–23, 24 | the run's tip, the start of its last segment, and that segment's length: one screen scale for every vertex of the run |
577
+ * | 25–28 | straight RGBA |
578
+ */
579
+ declare function packLines(lines: readonly Line3[]): Float32Array;
580
+
581
+ /** Options for {@link LinePass.draw}. */
582
+ interface LineDrawOptions {
583
+ /** Hide the parts of a line behind what the bound depth buffer holds. Default true. */
584
+ depthTest?: boolean;
585
+ }
586
+ /**
587
+ * Lines drawn into a WebGL2 context the host owns. The kernel hosts a renderer
588
+ * rather than owning one; this is a pass that renderer calls, between its own.
589
+ */
590
+ interface LinePass {
591
+ /**
592
+ * Draw `vertices`, as {@link packLines} lays them out, into the bound
593
+ * framebuffer under the host's `gl.viewport`. `viewProjection` takes world
594
+ * points to clip space; `size` is that viewport in CSS pixels, the unit a
595
+ * line's width is given in. Depth is tested, not written, and blending is
596
+ * straight alpha. The capabilities, depth mask, blend function, program,
597
+ * vertex array and array buffer it changes are put back as it found them.
598
+ */
599
+ draw(vertices: Float32Array, viewProjection: Mat4, size: {
600
+ width: number;
601
+ height: number;
602
+ }, options?: LineDrawOptions): void;
603
+ dispose(): void;
604
+ }
605
+ /** A {@link LinePass} on `gl`: one program, one vertex array, one buffer refilled each draw. */
606
+ declare function createLinePass(gl: WebGL2RenderingContext): LinePass;
607
+
467
608
  /**
468
609
  * Camera tools and their actions.
469
610
  *
@@ -556,4 +697,5 @@ declare const kernel3dKitSettingsSchema: Omit<_weasel_js_prefs.PrefGroup, "child
556
697
  };
557
698
  };
558
699
 
559
- export { type Aim3d, type Aim3dOptions, type Camera3d, type Camera3dDep, type ChromeBox, DEFAULT_ORBIT_LIMITS, type Footprint, type Frustum3d, type GroundGridSourceOptions, MAX_PITCH, type Node3d, type NodeBounds, type Orbit3dKitSettings, type OrbitLimits, POSE3_COMPOSITION, type Pose3, type Scene3d, type ScreenBox, type ScreenLine, type ScreenPoint, type Snap3dOptions, UNIT_CUBE, type Viewport3d, type ViewportSource, type World3d, aabbOfPose, aimOf3d, aimSpace3d, alignPlaneSource, boxSnapSource, cameraEye, cameraOfAim3d, cameraView, cameraViewProjection, clampCamera, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createNodeAtPoint, createPoseDescriptor, createSnap, dollyAction, dollyBy, dollyToward, groundGridSource, homeAction, kernel3dKitSettingsSchema, ndcToScreen, orbitAction, orbitBy, panAction, panBy, pinchDollyAction, pose3, poseMatrix, projectAabbToScreen, projectPoint, projectSnapMarks, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool, worldPoseOf };
700
+ export { DEFAULT_ORBIT_LIMITS, HEAD, HEAD_OVERDRAW, LINE_FLOATS, MAX_PITCH, POSE3_COMPOSITION, UNIT_CUBE, aabbOfPose, aimOf3d, aimSpace3d, alignPlaneSource, boxSnapSource, cameraEye, cameraOfAim3d, cameraView, cameraViewProjection, clampCamera, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createLinePass, createNodeAtPoint, createPoseDescriptor, createSnap, dollyAction, dollyBy, dollyToward, groundGridSource, headFor, homeAction, kernel3dKitSettingsSchema, layoutLine, ndcToScreen, orbitAction, orbitBy, packLines, panAction, panBy, pinchDollyAction, pose3, poseMatrix, projectAabbToScreen, projectPoint, projectSnapMarks, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool, visibleRuns, worldPoseOf };
701
+ export type { Aim3d, Aim3dOptions, Camera3d, Camera3dDep, ChromeBox, Footprint, Frustum3d, GroundGridSourceOptions, HeadShape, HeadSize, Line3, LineDrawOptions, LineLayout, LinePass, LineRun3, Node3d, NodeBounds, Orbit3dKitSettings, OrbitLimits, Pose3, Scene3d, ScreenBox, ScreenLine, ScreenPoint, Snap3dOptions, Viewport3d, ViewportSource, VisibleRun3, World3d };
package/dist/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import { lookAt, multiply, perspective, add, scale, quatIdentity, compose, transformAabb, quatMultiply, quatConjugate, quatRotate, sub, invert, transformPoint, normalize, transformPoint4, distance, dot, len, aabbOfPoints, intersectRayPlane, pointOnRay, cross, intersectRayAabb } from '@weasel-js/geom/3d';
2
- import { clamp, rectsOverlap } from '@weasel-js/geom';
3
- import { scenePoseFrame, createSnapOutcomeStore, assertSnapSourceIds, DEFAULT_SNAP_PX, overlaysOf, resolveOverlays, defineTool, snapSourcesFor, resolvedSnapMarks, SNAP_AXIS_STEP, gatherSnapCandidates, scenePoseAdapter, composeWorldPose } from '@weasel-js/core';
2
+ import { clamp, lengthTable, curveSpan, rectsOverlap } from '@weasel-js/geom';
3
+ import { scenePoseFrame, createSnapOutcomeStore, assertSnapSourceIds, DEFAULT_SNAP_PX, resolveOverlays, snapSourcesFor, resolvedSnapMarks, SNAP_AXIS_STEP, gatherSnapCandidates, scenePoseAdapter, composeWorldPose } from '@weasel-js/core/engine';
4
4
  import { isDev } from '@weasel-js/react';
5
5
  import { closestOnFlat, resolveSnap, createBvh, stepLine, createSettledCache } from '@weasel-js/geom/nd';
6
+ import { overlaysOf, defineTool } from '@weasel-js/routing';
7
+ import { resolveTrim } from '@weasel-js/stroke';
6
8
  import { useMemo } from 'react';
7
9
  import { pref } from '@weasel-js/prefs';
8
10
 
@@ -1125,6 +1127,341 @@ function collectOverlayBoxes(handles, origin) {
1125
1127
  }
1126
1128
  return boxes;
1127
1129
  }
1130
+
1131
+ // src/lines/head.ts
1132
+ var HEAD = { lenRatio: 8, maxShare: 0.5, aspect: 7 / 16 };
1133
+ var HEAD_OVERDRAW = 0.1;
1134
+ function headFor(shaftHalf, linePx = Number.POSITIVE_INFINITY, head = HEAD) {
1135
+ const wanted = shaftHalf * head.lenRatio;
1136
+ const headLen = Math.max(0, Math.min(wanted, head.maxShare * linePx));
1137
+ const headHalf = headLen * head.aspect;
1138
+ const shrunk = wanted > 0 ? shaftHalf * headLen / wanted : shaftHalf;
1139
+ return { headLen, headHalf, shaftHalf: wanted > 0 ? Math.min(shrunk, headHalf) : shrunk };
1140
+ }
1141
+ function layoutLine(shaftHalf, length, pxPerUnit, head) {
1142
+ if (!head || !(pxPerUnit > 0)) return { headLen: 0, headHalf: 0, shaftHalf, shaftEnd: length };
1143
+ const size = headFor(shaftHalf, pxPerUnit * length, head);
1144
+ return { ...size, shaftEnd: length - size.headLen / pxPerUnit * (1 - HEAD_OVERDRAW) };
1145
+ }
1146
+ var LINE_FLOATS = 29;
1147
+ var ROLE_BACK = 0;
1148
+ var ROLE_SHAFT_END = 1;
1149
+ var ROLE_HEAD_BASE = 2;
1150
+ var ROLE_TIP = 3;
1151
+ var SHAFT_CORNERS = [
1152
+ [ROLE_BACK, -1],
1153
+ [ROLE_BACK, 1],
1154
+ [ROLE_SHAFT_END, 1],
1155
+ [ROLE_BACK, -1],
1156
+ [ROLE_SHAFT_END, 1],
1157
+ [ROLE_SHAFT_END, -1]
1158
+ ];
1159
+ var HEAD_CORNERS = [[ROLE_HEAD_BASE, -1], [ROLE_HEAD_BASE, 1], [ROLE_TIP, 0]];
1160
+ var flat = (points) => points.flatMap((p) => [p.x, p.y, p.z]);
1161
+ var lerp3 = (a, b, t) => ({ x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t, z: a.z + (b.z - a.z) * t });
1162
+ var dist = (a, b) => Math.hypot(b.x - a.x, b.y - a.y, b.z - a.z);
1163
+ function visibleRuns(line) {
1164
+ const runs = line.runs.filter((r) => r.points.length >= 2);
1165
+ if (runs.length === 0) return [];
1166
+ const table = lengthTable(runs.map((r) => ({ points: flat(r.points), closed: !!r.closed })), 3);
1167
+ const span = resolveTrim(table, line.trim);
1168
+ const cut = span && curveSpan(table, span);
1169
+ if (!cut) return [];
1170
+ const out = [];
1171
+ for (let r = cut.from.run; r <= cut.to.run; r++) {
1172
+ const run = windowRun(runs[r], r, cut);
1173
+ if (run) out.push({ ...run, headed: !run.closed && !runs[r].closed && line.head !== null });
1174
+ }
1175
+ return out;
1176
+ }
1177
+ function windowRun(run, r, cut) {
1178
+ const pts = run.points;
1179
+ const n = pts.length;
1180
+ const segments = run.closed ? n : n - 1;
1181
+ const at = (i) => pts[i % n];
1182
+ const from = r === cut.from.run ? cut.from : { segment: 0, t: 0 };
1183
+ const to = r === cut.to.run ? cut.to : { segment: segments - 1, t: 1 };
1184
+ if (run.closed && from.segment === 0 && from.t === 0 && to.segment === segments - 1 && to.t === 1) {
1185
+ return { points: [...pts], closed: true };
1186
+ }
1187
+ const out = [lerp3(at(from.segment), at(from.segment + 1), from.t)];
1188
+ for (let k = from.segment + 1; k <= to.segment; k++) out.push(at(k));
1189
+ out.push(lerp3(at(to.segment), at(to.segment + 1), to.t));
1190
+ const kept = out.filter((p, i) => i === 0 || dist(out[i - 1], p) > 0);
1191
+ return kept.length >= 2 ? { points: kept, closed: false } : null;
1192
+ }
1193
+ function packLines(lines) {
1194
+ const chunks = [];
1195
+ for (const line of lines) {
1196
+ const shaftHalf = (line.width ?? 4) / 2;
1197
+ const head = line.head === void 0 ? HEAD : line.head;
1198
+ const toWorld = line.model ? (p) => transformPoint(line.model, p) : (p) => p;
1199
+ for (const run of visibleRuns(line)) {
1200
+ packRun(chunks, run, run.points.map(toWorld), shaftHalf, run.headed ? head : null, line.color);
1201
+ }
1202
+ }
1203
+ return Float32Array.from(chunks);
1204
+ }
1205
+ function packRun(out, run, world, shaftHalf, head, color) {
1206
+ const n = world.length;
1207
+ const segments = run.closed ? n : n - 1;
1208
+ const at = (i) => world[(i % n + n) % n];
1209
+ const along = [0];
1210
+ for (let i = 0; i < segments; i++) along.push(along[i] + dist(run.points[i], run.points[(i + 1) % n]));
1211
+ const total = along[segments];
1212
+ const apex = at(segments);
1213
+ const apexBack = at(segments - 1);
1214
+ const reach = total - along[segments - 1];
1215
+ const shape = head ? [head.lenRatio, head.maxShare, head.aspect] : [0, 0, 0];
1216
+ for (let i = 0; i < segments; i++) {
1217
+ const a = at(i);
1218
+ const b = at(i + 1);
1219
+ const before = run.closed || i > 0 ? at(i - 1) : a;
1220
+ const after = run.closed || i + 2 <= n - 1 ? at(i + 2) : b;
1221
+ const corners = head && i === segments - 1 ? [...SHAFT_CORNERS, ...HEAD_CORNERS] : SHAFT_CORNERS;
1222
+ for (const [role, side] of corners) {
1223
+ const adj = role === ROLE_BACK ? before : role === ROLE_SHAFT_END ? after : b;
1224
+ out.push(
1225
+ a.x,
1226
+ a.y,
1227
+ a.z,
1228
+ b.x,
1229
+ b.y,
1230
+ b.z,
1231
+ adj.x,
1232
+ adj.y,
1233
+ adj.z,
1234
+ role,
1235
+ side,
1236
+ along[i],
1237
+ along[i + 1],
1238
+ total,
1239
+ shaftHalf,
1240
+ ...shape,
1241
+ apex.x,
1242
+ apex.y,
1243
+ apex.z,
1244
+ apexBack.x,
1245
+ apexBack.y,
1246
+ apexBack.z,
1247
+ reach,
1248
+ ...color
1249
+ );
1250
+ }
1251
+ }
1252
+ }
1253
+
1254
+ // src/lines/pass.glsl.ts
1255
+ var LINE_VS = `#version 300 es
1256
+ precision highp float;
1257
+ layout(location = 0) in vec3 a_from;
1258
+ layout(location = 1) in vec3 a_to;
1259
+ layout(location = 2) in vec3 a_adj;
1260
+ layout(location = 3) in vec2 a_corner;
1261
+ layout(location = 4) in vec3 a_along;
1262
+ layout(location = 5) in vec4 a_shape;
1263
+ layout(location = 6) in vec3 a_apex;
1264
+ layout(location = 7) in vec4 a_apexBack;
1265
+ layout(location = 8) in vec4 a_color;
1266
+ uniform mat4 u_viewProjection;
1267
+ /* Half the viewport in CSS pixels: ndc times this is pixels. */
1268
+ uniform vec2 u_halfViewport;
1269
+ out vec4 v_color;
1270
+ const float OVERDRAW = ${HEAD_OVERDRAW.toFixed(3)};
1271
+ /* How far a bisector may stretch before the corner is let go; a hairpin would run to infinity. */
1272
+ const float MITER_LIMIT = 4.0;
1273
+ const vec4 CULLED = vec4(0.0, 0.0, 2.0, 1.0);
1274
+
1275
+ vec2 screenOf(vec4 c) { return c.xy / c.w * u_halfViewport; }
1276
+
1277
+ void main() {
1278
+ v_color = a_color;
1279
+ vec4 ca = u_viewProjection * vec4(a_from, 1.0);
1280
+ vec4 cb = u_viewProjection * vec4(a_to, 1.0);
1281
+ /* An end behind the eye projects to a line across the whole screen; drop the segment instead. */
1282
+ if (ca.w <= 0.0 || cb.w <= 0.0) { gl_Position = CULLED; return; }
1283
+ vec2 sa = screenOf(ca);
1284
+ vec2 sb = screenOf(cb);
1285
+ vec2 d = sb - sa;
1286
+ float L = length(d);
1287
+ vec2 dir = L > 0.0 ? d / L : vec2(1.0, 0.0);
1288
+ vec2 perp = vec2(-dir.y, dir.x);
1289
+ float role = a_corner.x;
1290
+ float side = a_corner.y;
1291
+ float s0 = a_along.x;
1292
+ float s1 = a_along.y;
1293
+ float total = a_along.z;
1294
+ float span = s1 - s0;
1295
+
1296
+ /* Pixels per unit near the tip, the same for every segment of the run; this segment's own where the tip is unseen. */
1297
+ float density = span > 0.0 ? L / span : 0.0;
1298
+ vec4 cp = u_viewProjection * vec4(a_apex, 1.0);
1299
+ vec4 cq = u_viewProjection * vec4(a_apexBack.xyz, 1.0);
1300
+ float reach = a_apexBack.w;
1301
+ if (reach > 0.0 && cp.w > 0.0 && cq.w > 0.0) {
1302
+ float px = length(screenOf(cp) - screenOf(cq));
1303
+ if (px > 0.0) density = px / reach;
1304
+ }
1305
+
1306
+ /* layoutLine in head.ts, repeated. */
1307
+ float shaftHalf = a_shape.x;
1308
+ float wanted = shaftHalf * a_shape.y;
1309
+ float headPx = 0.0;
1310
+ float headHalf = 0.0;
1311
+ float shaft = shaftHalf;
1312
+ float shaftEnd = total;
1313
+ if (wanted > 0.0 && density > 0.0) {
1314
+ headPx = max(0.0, min(wanted, a_shape.z * density * total));
1315
+ headHalf = headPx * a_shape.w;
1316
+ shaft = min(shaftHalf * headPx / wanted, headHalf);
1317
+ shaftEnd = total - headPx / density * (1.0 - OVERDRAW);
1318
+ }
1319
+
1320
+ if (role > ${(ROLE_SHAFT_END + 0.5).toFixed(1)}) {
1321
+ if (headPx <= 0.0) { gl_Position = CULLED; return; }
1322
+ if (role > ${(ROLE_TIP - 0.5).toFixed(1)}) { gl_Position = cb; return; }
1323
+ vec2 base = sb - dir * headPx + perp * side * headHalf;
1324
+ gl_Position = vec4(base / u_halfViewport * cb.w, cb.z, cb.w);
1325
+ return;
1326
+ }
1327
+
1328
+ float to = min(s1, shaftEnd);
1329
+ if (to <= s0 || span <= 0.0) { gl_Position = CULLED; return; }
1330
+ bool atEnd = role > 0.5;
1331
+ /* Clip space is linear in the world, so a fraction of the segment lands at its true depth. */
1332
+ vec4 c = mix(ca, cb, atEnd ? (to - s0) / span : 0.0);
1333
+ vec2 miter = perp;
1334
+ float stretch = 1.0;
1335
+ /* Only an end that meets its neighbor miters; a shaft cut short under the head stays square. */
1336
+ if (!atEnd || to >= s1) {
1337
+ vec4 cj = u_viewProjection * vec4(a_adj, 1.0);
1338
+ if (cj.w > 0.0) {
1339
+ vec2 sj = screenOf(cj);
1340
+ vec2 e = atEnd ? sj - sb : sa - sj;
1341
+ float eL = length(e);
1342
+ if (eL > 0.0) {
1343
+ vec2 o = e / eL;
1344
+ vec2 bisect = perp + vec2(-o.y, o.x);
1345
+ float bL = length(bisect);
1346
+ if (bL > 0.0001) {
1347
+ vec2 m = bisect / bL;
1348
+ float cosHalf = dot(m, perp);
1349
+ if (cosHalf > 1.0 / MITER_LIMIT) { miter = m; stretch = 1.0 / cosHalf; }
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1354
+ c.xy += miter * side * shaft * stretch / u_halfViewport * c.w;
1355
+ gl_Position = c;
1356
+ }`;
1357
+ var LINE_FS = `#version 300 es
1358
+ precision mediump float;
1359
+ in vec4 v_color;
1360
+ out vec4 fragColor;
1361
+ void main() {
1362
+ fragColor = v_color;
1363
+ }`;
1364
+
1365
+ // src/lines/pass.ts
1366
+ var ATTRIBUTES = [
1367
+ [3, 0],
1368
+ [3, 3],
1369
+ [3, 6],
1370
+ [2, 9],
1371
+ [3, 11],
1372
+ [4, 14],
1373
+ [3, 18],
1374
+ [4, 21],
1375
+ [4, 25]
1376
+ ];
1377
+ function compile(gl, type, source) {
1378
+ const shader = gl.createShader(type);
1379
+ gl.shaderSource(shader, source);
1380
+ gl.compileShader(shader);
1381
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS) && !gl.isContextLost()) {
1382
+ const log = gl.getShaderInfoLog(shader);
1383
+ gl.deleteShader(shader);
1384
+ throw new Error(`kernel3d line pass: shader failed to compile: ${log}`);
1385
+ }
1386
+ return shader;
1387
+ }
1388
+ function link(gl) {
1389
+ const program = gl.createProgram();
1390
+ const vs = compile(gl, gl.VERTEX_SHADER, LINE_VS);
1391
+ const fs = compile(gl, gl.FRAGMENT_SHADER, LINE_FS);
1392
+ gl.attachShader(program, vs);
1393
+ gl.attachShader(program, fs);
1394
+ gl.linkProgram(program);
1395
+ gl.deleteShader(vs);
1396
+ gl.deleteShader(fs);
1397
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS) && !gl.isContextLost()) {
1398
+ const log = gl.getProgramInfoLog(program);
1399
+ gl.deleteProgram(program);
1400
+ throw new Error(`kernel3d line pass: program failed to link: ${log}`);
1401
+ }
1402
+ return program;
1403
+ }
1404
+ function createLinePass(gl) {
1405
+ const program = link(gl);
1406
+ const viewProjection = gl.getUniformLocation(program, "u_viewProjection");
1407
+ const halfViewport = gl.getUniformLocation(program, "u_halfViewport");
1408
+ const buffer = gl.createBuffer();
1409
+ const vao = gl.createVertexArray();
1410
+ const prevVao = gl.getParameter(gl.VERTEX_ARRAY_BINDING);
1411
+ const prevBuffer = gl.getParameter(gl.ARRAY_BUFFER_BINDING);
1412
+ gl.bindVertexArray(vao);
1413
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1414
+ ATTRIBUTES.forEach(([size, offset], loc) => {
1415
+ gl.enableVertexAttribArray(loc);
1416
+ gl.vertexAttribPointer(loc, size, gl.FLOAT, false, LINE_FLOATS * 4, offset * 4);
1417
+ });
1418
+ gl.bindVertexArray(prevVao);
1419
+ gl.bindBuffer(gl.ARRAY_BUFFER, prevBuffer);
1420
+ const matrix = new Float32Array(16);
1421
+ return {
1422
+ draw(vertices, vp, size, options = {}) {
1423
+ if (vertices.length === 0) return;
1424
+ const was = {
1425
+ program: gl.getParameter(gl.CURRENT_PROGRAM),
1426
+ vao: gl.getParameter(gl.VERTEX_ARRAY_BINDING),
1427
+ buffer: gl.getParameter(gl.ARRAY_BUFFER_BINDING),
1428
+ blend: gl.isEnabled(gl.BLEND),
1429
+ depth: gl.isEnabled(gl.DEPTH_TEST),
1430
+ cull: gl.isEnabled(gl.CULL_FACE),
1431
+ depthMask: gl.getParameter(gl.DEPTH_WRITEMASK),
1432
+ blendFunc: [gl.BLEND_SRC_RGB, gl.BLEND_DST_RGB, gl.BLEND_SRC_ALPHA, gl.BLEND_DST_ALPHA].map((p) => gl.getParameter(p))
1433
+ };
1434
+ gl.useProgram(program);
1435
+ matrix.set(vp);
1436
+ gl.uniformMatrix4fv(viewProjection, false, matrix);
1437
+ gl.uniform2f(halfViewport, Math.max(1, size.width) / 2, Math.max(1, size.height) / 2);
1438
+ gl.enable(gl.BLEND);
1439
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
1440
+ gl.disable(gl.CULL_FACE);
1441
+ if (options.depthTest ?? true) gl.enable(gl.DEPTH_TEST);
1442
+ else gl.disable(gl.DEPTH_TEST);
1443
+ gl.depthMask(false);
1444
+ gl.bindVertexArray(vao);
1445
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1446
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.DYNAMIC_DRAW);
1447
+ gl.drawArrays(gl.TRIANGLES, 0, vertices.length / LINE_FLOATS);
1448
+ gl.useProgram(was.program);
1449
+ gl.bindVertexArray(was.vao);
1450
+ gl.bindBuffer(gl.ARRAY_BUFFER, was.buffer);
1451
+ const set = (cap, on) => on ? gl.enable(cap) : gl.disable(cap);
1452
+ set(gl.BLEND, was.blend);
1453
+ set(gl.DEPTH_TEST, was.depth);
1454
+ set(gl.CULL_FACE, was.cull);
1455
+ gl.depthMask(was.depthMask);
1456
+ gl.blendFuncSeparate(...was.blendFunc);
1457
+ },
1458
+ dispose() {
1459
+ gl.deleteVertexArray(vao);
1460
+ gl.deleteBuffer(buffer);
1461
+ gl.deleteProgram(program);
1462
+ }
1463
+ };
1464
+ }
1128
1465
  var ORBIT_RATE = 6e-3;
1129
1466
  var DOLLY_RATE = 15e-4;
1130
1467
  function screenDelta(move) {
@@ -1246,7 +1583,7 @@ function useOrbitTool() {
1246
1583
  id: "orbit",
1247
1584
  hookName: "useOrbitTool",
1248
1585
  cursor: "grab",
1249
- presentation: { label: "Orbit", group: "viewport" },
1586
+ presentation: { label: "Orbit", group: "view" },
1250
1587
  bindings: [
1251
1588
  { spec: { kind: "drag" }, actionId: "camera.orbit", opts: { id: "orbit.orbit" } },
1252
1589
  { spec: { kind: "drag", mods: { shift: true } }, actionId: "camera.pan", opts: { id: "orbit.pan" } }
@@ -1260,6 +1597,6 @@ var kernel3dKitSettingsSchema = pref.group("3D camera", {
1260
1597
  })
1261
1598
  });
1262
1599
 
1263
- export { DEFAULT_ORBIT_LIMITS, MAX_PITCH, POSE3_COMPOSITION, UNIT_CUBE, aabbOfPose, aimOf3d, aimSpace3d, alignPlaneSource, boxSnapSource, cameraEye, cameraOfAim3d, cameraView, cameraViewProjection, clampCamera, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createNodeAtPoint, createPoseDescriptor, createSnap, dollyAction, dollyBy, dollyToward, groundGridSource, homeAction, kernel3dKitSettingsSchema, ndcToScreen, orbitAction, orbitBy, panAction, panBy, pinchDollyAction, pose3, poseMatrix, projectAabbToScreen, projectPoint, projectSnapMarks, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool, worldPoseOf };
1600
+ export { DEFAULT_ORBIT_LIMITS, HEAD, HEAD_OVERDRAW, LINE_FLOATS, MAX_PITCH, POSE3_COMPOSITION, UNIT_CUBE, aabbOfPose, aimOf3d, aimSpace3d, alignPlaneSource, boxSnapSource, cameraEye, cameraOfAim3d, cameraView, cameraViewProjection, clampCamera, collectOverlayBoxes, createAreaSelect, createCamera, createInsert, createLinePass, createNodeAtPoint, createPoseDescriptor, createSnap, dollyAction, dollyBy, dollyToward, groundGridSource, headFor, homeAction, kernel3dKitSettingsSchema, layoutLine, ndcToScreen, orbitAction, orbitBy, packLines, panAction, panBy, pinchDollyAction, pose3, poseMatrix, projectAabbToScreen, projectPoint, projectSnapMarks, rayThroughScreenPoint, screenBoxOf, screenToNdc, useOrbitTool, visibleRuns, worldPoseOf };
1264
1601
  //# sourceMappingURL=index.js.map
1265
1602
  //# sourceMappingURL=index.js.map