@toolpath/viewer 2.2.0 → 2.3.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.
@@ -1,17 +1,17 @@
1
1
  // src/engine/engine-part.tsx
2
- import { useEffect as useEffect7, useMemo as useMemo7 } from "react";
2
+ import { useEffect as useEffect8, useMemo as useMemo8 } from "react";
3
3
 
4
4
  // src/part-mesh.tsx
5
- import { useThree as useThree7 } from "@react-three/fiber";
5
+ import { useThree as useThree8 } from "@react-three/fiber";
6
6
  import {
7
7
  useCallback as useCallback3,
8
- useEffect as useEffect6,
9
- useLayoutEffect as useLayoutEffect2,
10
- useMemo as useMemo6,
8
+ useEffect as useEffect7,
9
+ useLayoutEffect as useLayoutEffect3,
10
+ useMemo as useMemo7,
11
11
  useRef as useRef6,
12
12
  useSyncExternalStore
13
13
  } from "react";
14
- import { Vector3 as Vector311 } from "three";
14
+ import { Vector3 as Vector312 } from "three";
15
15
 
16
16
  // src/part-context.tsx
17
17
  import { createContext, useContext } from "react";
@@ -136,6 +136,8 @@ var DEFAULT_THEME = {
136
136
  picked: 16347926,
137
137
  edge: 0,
138
138
  edgeOpacity: 0.5,
139
+ // Red, which the direction palette's note keeps free for sharp corners.
140
+ sharpCorner: 14427686,
139
141
  sectionCap: 14672618,
140
142
  sectionHatch: 9080998,
141
143
  sectionOutline: 7057587,
@@ -157,7 +159,7 @@ function resolveTheme(overrides) {
157
159
  return { ...DEFAULT_THEME, ...overrides };
158
160
  }
159
161
  function themesEqual(a, b) {
160
- return a.background === b.background && a.hemisphereSky === b.hemisphereSky && a.hemisphereGround === b.hemisphereGround && a.hemisphereIntensity === b.hemisphereIntensity && a.ambient === b.ambient && a.ambientIntensity === b.ambientIntensity && a.part === b.part && a.partEmissive === b.partEmissive && a.hover === b.hover && a.highlight === b.highlight && a.picked === b.picked && a.edge === b.edge && a.edgeOpacity === b.edgeOpacity && a.sectionCap === b.sectionCap && a.sectionHatch === b.sectionHatch && a.sectionOutline === b.sectionOutline && a.sectionHandle === b.sectionHandle && a.sectionHandleOutline === b.sectionHandleOutline && a.measure === b.measure && a.measureSnap === b.measureSnap && a.cube === b.cube && a.cubeEdge === b.cubeEdge && a.cubeLabel === b.cubeLabel;
162
+ return a.background === b.background && a.hemisphereSky === b.hemisphereSky && a.hemisphereGround === b.hemisphereGround && a.hemisphereIntensity === b.hemisphereIntensity && a.ambient === b.ambient && a.ambientIntensity === b.ambientIntensity && a.part === b.part && a.partEmissive === b.partEmissive && a.hover === b.hover && a.highlight === b.highlight && a.picked === b.picked && a.edge === b.edge && a.edgeOpacity === b.edgeOpacity && a.sharpCorner === b.sharpCorner && a.sectionCap === b.sectionCap && a.sectionHatch === b.sectionHatch && a.sectionOutline === b.sectionOutline && a.sectionHandle === b.sectionHandle && a.sectionHandleOutline === b.sectionHandleOutline && a.measure === b.measure && a.measureSnap === b.measureSnap && a.cube === b.cube && a.cubeEdge === b.cubeEdge && a.cubeLabel === b.cubeLabel;
161
163
  }
162
164
  function directionColor(index) {
163
165
  const wrapped = index % DIRECTION_COLORS.length;
@@ -857,19 +859,19 @@ function useTapGuard() {
857
859
  // src/render/part.ts
858
860
  import {
859
861
  Box3,
860
- Color,
862
+ Color as Color2,
861
863
  DataTexture,
862
- DoubleSide,
863
- Float32BufferAttribute as Float32BufferAttribute2,
864
- Group,
864
+ DoubleSide as DoubleSide2,
865
+ Float32BufferAttribute as Float32BufferAttribute3,
866
+ Group as Group2,
865
867
  LineBasicMaterial,
866
868
  LineSegments,
867
- Mesh,
869
+ Mesh as Mesh2,
868
870
  MeshLambertMaterial,
869
871
  RedFormat,
870
872
  RGBAFormat,
871
873
  UnsignedByteType,
872
- Vector3 as Vector34
874
+ Vector3 as Vector35
873
875
  } from "three";
874
876
 
875
877
  // src/render/edges.ts
@@ -937,6 +939,555 @@ function regionEdgesGeometry(geometry, model) {
937
939
  return edges;
938
940
  }
939
941
 
942
+ // src/render/sharp-corners.ts
943
+ import {
944
+ BufferAttribute,
945
+ BufferGeometry,
946
+ Color,
947
+ DoubleSide,
948
+ Float32BufferAttribute as Float32BufferAttribute2,
949
+ GreaterDepth,
950
+ Group,
951
+ KeepStencilOp,
952
+ LessEqualDepth,
953
+ Mesh,
954
+ MeshBasicMaterial,
955
+ NotEqualStencilFunc,
956
+ ReplaceStencilOp,
957
+ ShaderMaterial,
958
+ Vector2 as Vector22,
959
+ Vector3 as Vector34,
960
+ Vector4
961
+ } from "three";
962
+
963
+ // src/render/adjacency.ts
964
+ var GRID = 1e-4;
965
+ var keyFor = (x, y, z) => `${Math.round(x / GRID)},${Math.round(y / GRID)},${Math.round(z / GRID)}`;
966
+ function regionAdjacency(model, geometry) {
967
+ const position = geometry.getAttribute("position");
968
+ const adjacency = /* @__PURE__ */ new Map();
969
+ if (!position) return adjacency;
970
+ const owner = new Int32Array(Math.floor(position.count / 3)).fill(-1);
971
+ for (const region of model.regions) {
972
+ for (let at = region.triangles.start; at < region.triangles.end; at++) {
973
+ if (at >= 0 && at < owner.length) owner[at] = region.idx;
974
+ }
975
+ }
976
+ const edges = /* @__PURE__ */ new Map();
977
+ const join = (a, b) => {
978
+ if (a === b) return;
979
+ const mine = adjacency.get(a) ?? /* @__PURE__ */ new Set();
980
+ mine.add(b);
981
+ adjacency.set(a, mine);
982
+ const theirs = adjacency.get(b) ?? /* @__PURE__ */ new Set();
983
+ theirs.add(a);
984
+ adjacency.set(b, theirs);
985
+ };
986
+ for (let triangle = 0; triangle < owner.length; triangle++) {
987
+ const region = owner[triangle];
988
+ if (region === -1) continue;
989
+ const corners = [0, 1, 2].map((corner) => {
990
+ const at = triangle * 3 + corner;
991
+ return keyFor(position.getX(at), position.getY(at), position.getZ(at));
992
+ });
993
+ for (let corner = 0; corner < 3; corner++) {
994
+ const from = corners[corner];
995
+ const to = corners[(corner + 1) % 3];
996
+ const edge = from < to ? `${from}|${to}` : `${to}|${from}`;
997
+ const seen = edges.get(edge);
998
+ if (seen === void 0) edges.set(edge, region);
999
+ else join(seen, region);
1000
+ }
1001
+ }
1002
+ return adjacency;
1003
+ }
1004
+
1005
+ // src/render/sharp-corners.ts
1006
+ var MIN_FOLD_DEGREES = 20;
1007
+ var ALONG_TOLERANCE_DEGREES = 15;
1008
+ var BAND_WIDTH = 6;
1009
+ var NEAR_OPACITY = 1;
1010
+ var FAR_OPACITY = 0.35;
1011
+ var DEGREES = 180 / Math.PI;
1012
+ function surfaceCreases(geometry, model) {
1013
+ const position = geometry.getAttribute("position");
1014
+ if (!position || geometry.index) return emptyCreases();
1015
+ const triangleCount = Math.floor(position.count / 3);
1016
+ const surfaces = visualSurfaces(model.regions);
1017
+ const surfaceOf = new Int32Array(triangleCount).fill(-1);
1018
+ const regionOf = new Int32Array(triangleCount).fill(-1);
1019
+ for (const region of model.regions) {
1020
+ const end = Math.min(region.triangles.end, triangleCount);
1021
+ for (let triangle = Math.max(region.triangles.start, 0); triangle < end; triangle += 1) {
1022
+ surfaceOf[triangle] = surfaces.get(region.idx) ?? region.idx;
1023
+ regionOf[triangle] = region.idx;
1024
+ }
1025
+ }
1026
+ const normals = new Float32Array(triangleCount * 3);
1027
+ for (let triangle = 0; triangle < triangleCount; triangle += 1) {
1028
+ const at = triangle * 3;
1029
+ const ax = position.getX(at);
1030
+ const ay = position.getY(at);
1031
+ const az = position.getZ(at);
1032
+ const ux = position.getX(at + 1) - ax;
1033
+ const uy = position.getY(at + 1) - ay;
1034
+ const uz = position.getZ(at + 1) - az;
1035
+ const vx = position.getX(at + 2) - ax;
1036
+ const vy = position.getY(at + 2) - ay;
1037
+ const vz = position.getZ(at + 2) - az;
1038
+ const nx = uy * vz - uz * vy;
1039
+ const ny = uz * vx - ux * vz;
1040
+ const nz = ux * vy - uy * vx;
1041
+ const length = Math.hypot(nx, ny, nz);
1042
+ if (length === 0) continue;
1043
+ normals[at] = nx / length;
1044
+ normals[at + 1] = ny / length;
1045
+ normals[at + 2] = nz / length;
1046
+ }
1047
+ const keyOf = (vertex) => keyFor(position.getX(vertex), position.getY(vertex), position.getZ(vertex));
1048
+ const open = /* @__PURE__ */ new Map();
1049
+ const positions = [];
1050
+ const angles = [];
1051
+ const concave = [];
1052
+ const regions = [];
1053
+ const fars = [];
1054
+ const inFront = (triangle, vertex, on) => normals[triangle * 3] * (position.getX(vertex) - position.getX(on)) + normals[triangle * 3 + 1] * (position.getY(vertex) - position.getY(on)) + normals[triangle * 3 + 2] * (position.getZ(vertex) - position.getZ(on));
1055
+ const measure = (first, second) => {
1056
+ const t1 = Math.floor(first / 3);
1057
+ const t2 = Math.floor(second / 3);
1058
+ if (surfaceOf[t1] === surfaceOf[t2]) return;
1059
+ if (normals[t1 * 3] === 0 && normals[t1 * 3 + 1] === 0 && normals[t1 * 3 + 2] === 0) return;
1060
+ if (normals[t2 * 3] === 0 && normals[t2 * 3 + 1] === 0 && normals[t2 * 3 + 2] === 0) return;
1061
+ const dot2 = normals[t1 * 3] * normals[t2 * 3] + normals[t1 * 3 + 1] * normals[t2 * 3 + 1] + normals[t1 * 3 + 2] * normals[t2 * 3 + 2];
1062
+ const a = t1 * 3 + first % 3;
1063
+ const b = t1 * 3 + (first + 1) % 3;
1064
+ const far1 = t1 * 3 + (first + 2) % 3;
1065
+ const far2 = t2 * 3 + (second + 2) % 3;
1066
+ const fold = inFront(t1, far2, a) + inFront(t2, far1, a);
1067
+ positions.push(
1068
+ position.getX(a),
1069
+ position.getY(a),
1070
+ position.getZ(a),
1071
+ position.getX(b),
1072
+ position.getY(b),
1073
+ position.getZ(b)
1074
+ );
1075
+ angles.push(Math.acos(Math.min(1, Math.max(-1, dot2))) * DEGREES);
1076
+ concave.push(fold > 0 ? 1 : 0);
1077
+ regions.push(regionOf[t1], regionOf[t2]);
1078
+ fars.push(
1079
+ position.getX(far1),
1080
+ position.getY(far1),
1081
+ position.getZ(far1),
1082
+ position.getX(far2),
1083
+ position.getY(far2),
1084
+ position.getZ(far2)
1085
+ );
1086
+ };
1087
+ for (let triangle = 0; triangle < triangleCount; triangle += 1) {
1088
+ const keys = [keyOf(triangle * 3), keyOf(triangle * 3 + 1), keyOf(triangle * 3 + 2)];
1089
+ for (let corner = 0; corner < 3; corner += 1) {
1090
+ const from = keys[corner];
1091
+ const to = keys[(corner + 1) % 3];
1092
+ if (from === to) continue;
1093
+ const edge = from < to ? `${from}|${to}` : `${to}|${from}`;
1094
+ const seen = open.get(edge);
1095
+ if (seen === void 0) open.set(edge, triangle * 3 + corner);
1096
+ else if (seen !== -1) {
1097
+ measure(seen, triangle * 3 + corner);
1098
+ open.set(edge, -1);
1099
+ }
1100
+ }
1101
+ }
1102
+ return {
1103
+ count: angles.length,
1104
+ positions: new Float32Array(positions),
1105
+ angles: new Float32Array(angles),
1106
+ concave: new Uint8Array(concave),
1107
+ regions: new Int32Array(regions),
1108
+ fars: new Float32Array(fars)
1109
+ };
1110
+ }
1111
+ function sharpCornerKey(options) {
1112
+ return options.features.map((tag) => tag.toLowerCase()).sort().join(",");
1113
+ }
1114
+ function findSharpCorners(creases, model, options) {
1115
+ const alongCos = Math.cos(ALONG_TOLERANCE_DEGREES / DEGREES);
1116
+ const flagged = new Set(options.features.map((tag) => tag.toLowerCase()));
1117
+ const owners = /* @__PURE__ */ new Map();
1118
+ for (const feature of model.features) {
1119
+ if (!flagged.has(feature.tag.toLowerCase())) continue;
1120
+ const axis = unit2(feature.machiningDirection);
1121
+ if (!axis) continue;
1122
+ for (const region of model.regionIndex.regionsForFeature(feature.tag)) {
1123
+ const held = owners.get(region);
1124
+ if (held) held.push({ tag: feature.tag, axis });
1125
+ else owners.set(region, [{ tag: feature.tag, axis }]);
1126
+ }
1127
+ }
1128
+ if (owners.size === 0) return [];
1129
+ const { positions } = creases;
1130
+ const kept = [];
1131
+ for (let segment = 0; segment < creases.count; segment += 1) {
1132
+ if (creases.concave[segment] !== 1 || creases.angles[segment] < MIN_FOLD_DEGREES) continue;
1133
+ const candidates = [
1134
+ ...owners.get(creases.regions[segment * 2]) ?? [],
1135
+ ...owners.get(creases.regions[segment * 2 + 1]) ?? []
1136
+ ];
1137
+ if (candidates.length === 0) continue;
1138
+ const at = segment * 6;
1139
+ const dx = positions[at + 3] - positions[at];
1140
+ const dy = positions[at + 4] - positions[at + 1];
1141
+ const dz = positions[at + 5] - positions[at + 2];
1142
+ const length = Math.hypot(dx, dy, dz);
1143
+ if (length === 0) continue;
1144
+ const owner = candidates.find(
1145
+ ({ axis }) => Math.abs(dx * axis.x + dy * axis.y + dz * axis.z) / length >= alongCos
1146
+ );
1147
+ if (owner) kept.push({ segment, feature: owner.tag });
1148
+ }
1149
+ const keyOf = (at) => keyFor(positions[at], positions[at + 1], positions[at + 2]);
1150
+ const parent = kept.map((_, at) => at);
1151
+ const root = (at) => {
1152
+ while (parent[at] !== at) at = parent[at] = parent[parent[at]];
1153
+ return at;
1154
+ };
1155
+ const byEnd = /* @__PURE__ */ new Map();
1156
+ kept.forEach(({ segment, feature }, at) => {
1157
+ for (const end of [segment * 6, segment * 6 + 3]) {
1158
+ const key = `${feature.toLowerCase()}|${keyOf(end)}`;
1159
+ const other = byEnd.get(key);
1160
+ if (other === void 0) byEnd.set(key, at);
1161
+ else parent[root(at)] = root(other);
1162
+ }
1163
+ });
1164
+ const groups = /* @__PURE__ */ new Map();
1165
+ kept.forEach((_, at) => {
1166
+ const group = groups.get(root(at));
1167
+ if (group) group.push(at);
1168
+ else groups.set(root(at), [at]);
1169
+ });
1170
+ return [...groups.values()].map((members, index) => {
1171
+ const feature = kept[members[0]].feature;
1172
+ const segments = members.map((at) => kept[at].segment);
1173
+ const first = segments[0] * 6;
1174
+ const axis = unit2({
1175
+ x: positions[first + 3] - positions[first],
1176
+ y: positions[first + 4] - positions[first + 1],
1177
+ z: positions[first + 5] - positions[first + 2]
1178
+ });
1179
+ let start = null;
1180
+ let end = null;
1181
+ let low = Infinity;
1182
+ let high = -Infinity;
1183
+ let length = 0;
1184
+ let folded = 0;
1185
+ const regions = /* @__PURE__ */ new Set();
1186
+ for (const segment of segments) {
1187
+ const at = segment * 6;
1188
+ for (const point of [at, at + 3]) {
1189
+ const p = { x: positions[point], y: positions[point + 1], z: positions[point + 2] };
1190
+ const along = p.x * axis.x + p.y * axis.y + p.z * axis.z;
1191
+ if (along < low) [low, start] = [along, p];
1192
+ if (along > high) [high, end] = [along, p];
1193
+ }
1194
+ const run = Math.hypot(
1195
+ positions[at + 3] - positions[at],
1196
+ positions[at + 4] - positions[at + 1],
1197
+ positions[at + 5] - positions[at + 2]
1198
+ );
1199
+ length += run;
1200
+ folded += run * creases.angles[segment];
1201
+ regions.add(creases.regions[segment * 2]);
1202
+ regions.add(creases.regions[segment * 2 + 1]);
1203
+ }
1204
+ return {
1205
+ index,
1206
+ feature,
1207
+ start,
1208
+ end,
1209
+ length,
1210
+ // The normals turn by the fold; the faces themselves open by what is left of 180°.
1211
+ angle: 180 - folded / length,
1212
+ regions: [...regions],
1213
+ segments
1214
+ };
1215
+ });
1216
+ }
1217
+ function sharpCornerPositions(creases, model, options) {
1218
+ const segments = findSharpCorners(creases, model, options).flatMap((corner) => corner.segments);
1219
+ const positions = new Float32Array(segments.length * 6);
1220
+ segments.forEach(
1221
+ (segment, at) => positions.set(creases.positions.subarray(segment * 6, segment * 6 + 6), at * 6)
1222
+ );
1223
+ return positions;
1224
+ }
1225
+ var SHARP_CORNER_HOVER_PIXELS = 6;
1226
+ function sharpCornerAt(corners, creases, hit, camera, viewport, matrixWorld) {
1227
+ const pointer = screenPoint(camera, hit.point, viewport);
1228
+ const a = new Vector34();
1229
+ const b = new Vector34();
1230
+ const from = new Vector22();
1231
+ const to = new Vector22();
1232
+ let nearest = null;
1233
+ let reach = SHARP_CORNER_HOVER_PIXELS;
1234
+ for (const corner of corners) {
1235
+ if (!corner.regions.includes(hit.region)) continue;
1236
+ for (const segment of corner.segments) {
1237
+ a.fromArray(creases.positions, segment * 6).applyMatrix4(matrixWorld);
1238
+ b.fromArray(creases.positions, segment * 6 + 3).applyMatrix4(matrixWorld);
1239
+ screenPoint(camera, a, viewport, from);
1240
+ screenPoint(camera, b, viewport, to);
1241
+ const distance = distanceToSegment(pointer, from, to);
1242
+ if (distance <= reach) [reach, nearest] = [distance, corner];
1243
+ }
1244
+ }
1245
+ return nearest;
1246
+ }
1247
+ function distanceToSegment(point, from, to) {
1248
+ const dx = to.x - from.x;
1249
+ const dy = to.y - from.y;
1250
+ const span = dx * dx + dy * dy;
1251
+ const t = span === 0 ? 0 : Math.min(1, Math.max(0, ((point.x - from.x) * dx + (point.y - from.y) * dy) / span));
1252
+ return Math.hypot(point.x - (from.x + t * dx), point.y - (from.y + t * dy));
1253
+ }
1254
+ function sharpCornerBands(creases, corners) {
1255
+ const position = [];
1256
+ const perp = [];
1257
+ const side = [];
1258
+ const reach = [];
1259
+ const owner = [];
1260
+ const index = [];
1261
+ for (const { index: corner, segments } of corners) {
1262
+ for (const segment of segments) {
1263
+ const at = segment * 6;
1264
+ const a = [creases.positions[at], creases.positions[at + 1], creases.positions[at + 2]];
1265
+ const b = [creases.positions[at + 3], creases.positions[at + 4], creases.positions[at + 5]];
1266
+ const edge = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
1267
+ const length = Math.hypot(edge[0], edge[1], edge[2]);
1268
+ if (length === 0) continue;
1269
+ const along = edge.map((value) => value / length);
1270
+ for (let face = 0; face < 2; face += 1) {
1271
+ const far = at + face * 3;
1272
+ const v = [
1273
+ creases.fars[far] - a[0],
1274
+ creases.fars[far + 1] - a[1],
1275
+ creases.fars[far + 2] - a[2]
1276
+ ];
1277
+ const t = v[0] * along[0] + v[1] * along[1] + v[2] * along[2];
1278
+ const across = [v[0] - t * along[0], v[1] - t * along[1], v[2] - t * along[2]];
1279
+ const extent = Math.hypot(across[0], across[1], across[2]);
1280
+ if (extent === 0) continue;
1281
+ const first = position.length / 3;
1282
+ for (const [point, out] of [
1283
+ [a, 0],
1284
+ [b, 0],
1285
+ [a, 1],
1286
+ [b, 1]
1287
+ ]) {
1288
+ position.push(point[0], point[1], point[2]);
1289
+ perp.push(across[0] / extent, across[1] / extent, across[2] / extent);
1290
+ side.push(out);
1291
+ reach.push(extent);
1292
+ owner.push(corner);
1293
+ }
1294
+ index.push(first, first + 1, first + 3, first, first + 3, first + 2);
1295
+ }
1296
+ }
1297
+ }
1298
+ const geometry = new BufferGeometry();
1299
+ geometry.setAttribute("position", new Float32BufferAttribute2(position, 3));
1300
+ geometry.setAttribute("aPerp", new Float32BufferAttribute2(perp, 3));
1301
+ geometry.setAttribute("aSide", new Float32BufferAttribute2(side, 1));
1302
+ geometry.setAttribute("aReach", new Float32BufferAttribute2(reach, 1));
1303
+ geometry.setAttribute("aCorner", new Float32BufferAttribute2(owner, 1));
1304
+ geometry.setIndex(new BufferAttribute(new Uint32Array(index), 1));
1305
+ return geometry;
1306
+ }
1307
+ var SHARP_CORNER_RENDER_ORDER = { depth: 6.25, near: 6.5, far: 6.75 };
1308
+ var SHARP_CORNER_STENCIL_BIT = 128;
1309
+ var BAND_VERTEX = (
1310
+ /* glsl */
1311
+ `
1312
+ #include <common>
1313
+ #include <clipping_planes_pars_vertex>
1314
+
1315
+ attribute vec3 aPerp;
1316
+ attribute float aSide;
1317
+ attribute float aReach;
1318
+ attribute float aCorner;
1319
+ uniform float uHalfWidth;
1320
+ uniform float uHovered;
1321
+ uniform vec2 uResolution;
1322
+ varying float vHovered;
1323
+
1324
+ void main() {
1325
+ vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
1326
+ vec3 across = mat3(modelViewMatrix) * aPerp;
1327
+ float reach = aReach * length(across);
1328
+ across = normalize(across);
1329
+
1330
+ bool perspective = projectionMatrix[2][3] == -1.0;
1331
+ float pixel = 2.0 / (projectionMatrix[1][1] * uResolution.y);
1332
+ if (perspective) pixel *= -mvPosition.z;
1333
+ vec3 sight = perspective ? normalize(-mvPosition.xyz) : vec3(0.0, 0.0, 1.0);
1334
+ float shown = length(across - sight * dot(across, sight));
1335
+ vHovered = abs(aCorner - uHovered) < 0.5 ? 1.0 : 0.0;
1336
+ float offset = min(uHalfWidth * pixel / max(shown, 0.25), reach);
1337
+
1338
+ mvPosition.xyz += across * offset * aSide;
1339
+ gl_Position = projectionMatrix * mvPosition;
1340
+ #include <clipping_planes_vertex>
1341
+ }
1342
+ `
1343
+ );
1344
+ var BAND_FRAGMENT = (
1345
+ /* glsl */
1346
+ `
1347
+ #include <common>
1348
+ #include <clipping_planes_pars_fragment>
1349
+
1350
+ uniform vec3 diffuse;
1351
+ uniform vec3 hoverColor;
1352
+ uniform float opacity;
1353
+ varying float vHovered;
1354
+
1355
+ void main() {
1356
+ #include <clipping_planes_fragment>
1357
+ // The corner under the pointer in the hover colour, as the part's faces are, not wider.
1358
+ vec3 color = mix(diffuse, hoverColor, vHovered);
1359
+ // And twice as strong: plain even where the part hides it.
1360
+ gl_FragColor = vec4(color, min(1.0, opacity * mix(1.0, 2.0, vHovered)));
1361
+ #include <colorspace_fragment>
1362
+ }
1363
+ `
1364
+ );
1365
+ function createSharpCornerDrawing(theme, part) {
1366
+ let geometry = new BufferGeometry();
1367
+ const resolution = { value: new Vector22(1, 1) };
1368
+ const hovered = { value: -1 };
1369
+ const viewport = new Vector4();
1370
+ const material = (opacity, depthFunc) => {
1371
+ const band = new ShaderMaterial({
1372
+ uniforms: {
1373
+ diffuse: { value: new Color(theme.sharpCorner) },
1374
+ hoverColor: { value: new Color(theme.hover) },
1375
+ opacity: { value: opacity },
1376
+ uHalfWidth: { value: BAND_WIDTH / 2 },
1377
+ uHovered: hovered,
1378
+ uResolution: resolution
1379
+ },
1380
+ vertexShader: BAND_VERTEX,
1381
+ fragmentShader: BAND_FRAGMENT,
1382
+ transparent: true,
1383
+ depthWrite: false,
1384
+ side: DoubleSide,
1385
+ clipping: true
1386
+ });
1387
+ band.depthFunc = depthFunc;
1388
+ band.polygonOffset = true;
1389
+ band.polygonOffsetFactor = -1;
1390
+ band.polygonOffsetUnits = -1;
1391
+ band.stencilWrite = true;
1392
+ band.stencilRef = SHARP_CORNER_STENCIL_BIT;
1393
+ band.stencilFuncMask = SHARP_CORNER_STENCIL_BIT;
1394
+ band.stencilWriteMask = SHARP_CORNER_STENCIL_BIT;
1395
+ band.stencilFunc = NotEqualStencilFunc;
1396
+ band.stencilFail = KeepStencilOp;
1397
+ band.stencilZFail = KeepStencilOp;
1398
+ band.stencilZPass = ReplaceStencilOp;
1399
+ return band;
1400
+ };
1401
+ const nearMaterial = material(NEAR_OPACITY, LessEqualDepth);
1402
+ const farMaterial = material(FAR_OPACITY, GreaterDepth);
1403
+ const near = new Mesh(geometry, nearMaterial);
1404
+ const far = new Mesh(geometry, farMaterial);
1405
+ near.name = "sharpCornersNear";
1406
+ far.name = "sharpCornersFar";
1407
+ near.renderOrder = SHARP_CORNER_RENDER_ORDER.near;
1408
+ far.renderOrder = SHARP_CORNER_RENDER_ORDER.far;
1409
+ near.raycast = () => {
1410
+ };
1411
+ far.raycast = () => {
1412
+ };
1413
+ const depthMaterial = new MeshBasicMaterial({
1414
+ colorWrite: false,
1415
+ depthWrite: true,
1416
+ transparent: true,
1417
+ polygonOffset: true,
1418
+ polygonOffsetFactor: 1,
1419
+ polygonOffsetUnits: 1
1420
+ });
1421
+ const depth = new Mesh(part, depthMaterial);
1422
+ depth.name = "sharpCornersDepth";
1423
+ depth.renderOrder = SHARP_CORNER_RENDER_ORDER.depth;
1424
+ depth.visible = false;
1425
+ depth.raycast = () => {
1426
+ };
1427
+ near.onBeforeRender = (renderer) => {
1428
+ renderer.getViewport(viewport);
1429
+ resolution.value.set(viewport.z, viewport.w);
1430
+ };
1431
+ const object = new Group();
1432
+ object.add(depth, far, near);
1433
+ object.visible = false;
1434
+ return {
1435
+ object,
1436
+ clear() {
1437
+ object.visible = false;
1438
+ },
1439
+ setHovered(index) {
1440
+ hovered.value = index ?? -1;
1441
+ },
1442
+ setCorners(creases, corners) {
1443
+ hovered.value = -1;
1444
+ object.visible = corners.length > 0;
1445
+ if (corners.length === 0) return;
1446
+ const next = sharpCornerBands(creases, corners);
1447
+ near.geometry = next;
1448
+ far.geometry = next;
1449
+ geometry.dispose();
1450
+ geometry = next;
1451
+ },
1452
+ setTheme(next) {
1453
+ for (const band of [nearMaterial, farMaterial]) {
1454
+ ;
1455
+ band.uniforms["diffuse"].value.setHex(next.sharpCorner);
1456
+ band.uniforms["hoverColor"].value.setHex(next.hover);
1457
+ }
1458
+ },
1459
+ setPartDepth(on) {
1460
+ depth.visible = on;
1461
+ },
1462
+ setClippingPlanes(planes) {
1463
+ nearMaterial.clippingPlanes = planes;
1464
+ farMaterial.clippingPlanes = planes;
1465
+ depthMaterial.clippingPlanes = planes;
1466
+ },
1467
+ dispose() {
1468
+ object.clear();
1469
+ geometry.dispose();
1470
+ nearMaterial.dispose();
1471
+ farMaterial.dispose();
1472
+ depthMaterial.dispose();
1473
+ }
1474
+ };
1475
+ }
1476
+ function unit2(vector) {
1477
+ const length = Math.hypot(vector.x, vector.y, vector.z);
1478
+ return length === 0 ? null : { x: vector.x / length, y: vector.y / length, z: vector.z / length };
1479
+ }
1480
+ function emptyCreases() {
1481
+ return {
1482
+ count: 0,
1483
+ positions: new Float32Array(0),
1484
+ angles: new Float32Array(0),
1485
+ concave: new Uint8Array(0),
1486
+ regions: new Int32Array(0),
1487
+ fars: new Float32Array(0)
1488
+ };
1489
+ }
1490
+
940
1491
  // src/render/part.ts
941
1492
  var REGION_ATTRIBUTE = "aRegion";
942
1493
  var EMISSIVE_MIX = 0.4;
@@ -959,7 +1510,7 @@ function buildRegionAttribute(model, texels, vertexCount) {
959
1510
  function createPart(model, geometry, theme) {
960
1511
  const position = geometry.getAttribute("position");
961
1512
  const texels = buildRegionTexels(model);
962
- const regionAttribute = new Float32BufferAttribute2(
1513
+ const regionAttribute = new Float32BufferAttribute3(
963
1514
  buildRegionAttribute(model, texels, position.count),
964
1515
  1
965
1516
  );
@@ -975,6 +1526,11 @@ function createPart(model, geometry, theme) {
975
1526
  const wireframe = { value: false };
976
1527
  let focused = false;
977
1528
  let currentTheme = theme;
1529
+ let clipping = null;
1530
+ let creases = null;
1531
+ let sharpLines = null;
1532
+ let sharpKey = "";
1533
+ let corners = [];
978
1534
  const material = new MeshLambertMaterial({
979
1535
  color: theme.part,
980
1536
  emissive: theme.partEmissive,
@@ -1038,11 +1594,11 @@ function createPart(model, geometry, theme) {
1038
1594
  emissive: theme.partEmissive,
1039
1595
  // A selected face can be viewed through the part from the side its normal
1040
1596
  // faces away from.
1041
- side: DoubleSide
1597
+ side: DoubleSide2
1042
1598
  });
1043
1599
  configureShader(focusMaterial, true);
1044
1600
  configureShader(material, false);
1045
- const mesh = new Mesh(geometry, material);
1601
+ const mesh = new Mesh2(geometry, material);
1046
1602
  mesh.renderOrder = 3;
1047
1603
  const edgeGeometry = regionEdgesGeometry(geometry, model);
1048
1604
  const edgeMaterial = new LineBasicMaterial({
@@ -1054,7 +1610,7 @@ function createPart(model, geometry, theme) {
1054
1610
  edges.renderOrder = 4;
1055
1611
  edges.raycast = () => {
1056
1612
  };
1057
- const focusMesh = new Mesh(geometry, focusMaterial);
1613
+ const focusMesh = new Mesh2(geometry, focusMaterial);
1058
1614
  focusMesh.renderOrder = 3;
1059
1615
  focusMesh.visible = false;
1060
1616
  focusMesh.raycast = () => {
@@ -1068,11 +1624,12 @@ function createPart(model, geometry, theme) {
1068
1624
  }
1069
1625
  focusState.value = focused;
1070
1626
  focusMesh.visible = focused;
1627
+ sharpLines?.setPartDepth(transparent);
1071
1628
  };
1072
- const object = new Group();
1629
+ const object = new Group2();
1073
1630
  object.add(focusMesh, mesh, edges);
1074
- const scratchColor = new Color();
1075
- const scratchVector = new Vector34();
1631
+ const scratchColor = new Color2();
1632
+ const scratchVector = new Vector35();
1076
1633
  const paintRegion = (region, color, weight) => {
1077
1634
  const column = texels.get(region);
1078
1635
  if (column === void 0) return;
@@ -1146,9 +1703,11 @@ function createPart(model, geometry, theme) {
1146
1703
  },
1147
1704
  setClippingPlanes(planes) {
1148
1705
  const value = planes === null ? null : [...planes];
1706
+ clipping = value;
1149
1707
  material.clippingPlanes = value;
1150
1708
  focusMaterial.clippingPlanes = value;
1151
1709
  edgeMaterial.clippingPlanes = value;
1710
+ sharpLines?.setClippingPlanes(value);
1152
1711
  },
1153
1712
  setTheme(next) {
1154
1713
  currentTheme = next;
@@ -1158,6 +1717,7 @@ function createPart(model, geometry, theme) {
1158
1717
  focusMaterial.emissive.setHex(next.partEmissive);
1159
1718
  edgeMaterial.color.setHex(wireframe.value ? next.part : next.edge);
1160
1719
  edgeMaterial.opacity = wireframe.value ? 1 : next.edgeOpacity;
1720
+ sharpLines?.setTheme(next);
1161
1721
  },
1162
1722
  setDisplay(display, showEdges = true) {
1163
1723
  const enabled = display === "wireframe";
@@ -1169,6 +1729,35 @@ function createPart(model, geometry, theme) {
1169
1729
  edgeMaterial.color.setHex(enabled ? currentTheme.part : currentTheme.edge);
1170
1730
  edgeMaterial.opacity = enabled ? 1 : currentTheme.edgeOpacity;
1171
1731
  },
1732
+ setSharpCorners(options) {
1733
+ const key = options === null ? "" : sharpCornerKey(options);
1734
+ if (key === sharpKey) return;
1735
+ sharpKey = key;
1736
+ if (options === null || options.features.length === 0) {
1737
+ corners = [];
1738
+ sharpLines?.clear();
1739
+ return;
1740
+ }
1741
+ creases ??= surfaceCreases(geometry, model);
1742
+ if (!sharpLines) {
1743
+ sharpLines = createSharpCornerDrawing(currentTheme, geometry);
1744
+ sharpLines.setClippingPlanes(clipping);
1745
+ sharpLines.setPartDepth(wireframe.value || focused);
1746
+ object.add(sharpLines.object);
1747
+ }
1748
+ corners = findSharpCorners(creases, model, options);
1749
+ sharpLines.setCorners(creases, corners);
1750
+ },
1751
+ sharpCorners() {
1752
+ return corners;
1753
+ },
1754
+ sharpCornerAt(hit, camera, viewport) {
1755
+ if (!creases || corners.length === 0) return null;
1756
+ return sharpCornerAt(corners, creases, hit, camera, viewport, mesh.matrixWorld);
1757
+ },
1758
+ setHoveredSharpCorner(index) {
1759
+ sharpLines?.setHovered(index);
1760
+ },
1172
1761
  dispose() {
1173
1762
  object.clear();
1174
1763
  if (geometry.getAttribute(REGION_ATTRIBUTE) === regionAttribute) {
@@ -1178,54 +1767,13 @@ function createPart(model, geometry, theme) {
1178
1767
  focusMaterial.dispose();
1179
1768
  edgeGeometry.dispose();
1180
1769
  edgeMaterial.dispose();
1770
+ sharpLines?.dispose();
1181
1771
  stateTexture.dispose();
1182
1772
  opacityTexture.dispose();
1183
1773
  }
1184
1774
  };
1185
1775
  }
1186
1776
 
1187
- // src/render/adjacency.ts
1188
- var GRID = 1e-4;
1189
- var keyFor = (x, y, z) => `${Math.round(x / GRID)},${Math.round(y / GRID)},${Math.round(z / GRID)}`;
1190
- function regionAdjacency(model, geometry) {
1191
- const position = geometry.getAttribute("position");
1192
- const adjacency = /* @__PURE__ */ new Map();
1193
- if (!position) return adjacency;
1194
- const owner = new Int32Array(Math.floor(position.count / 3)).fill(-1);
1195
- for (const region of model.regions) {
1196
- for (let at = region.triangles.start; at < region.triangles.end; at++) {
1197
- if (at >= 0 && at < owner.length) owner[at] = region.idx;
1198
- }
1199
- }
1200
- const edges = /* @__PURE__ */ new Map();
1201
- const join = (a, b) => {
1202
- if (a === b) return;
1203
- const mine = adjacency.get(a) ?? /* @__PURE__ */ new Set();
1204
- mine.add(b);
1205
- adjacency.set(a, mine);
1206
- const theirs = adjacency.get(b) ?? /* @__PURE__ */ new Set();
1207
- theirs.add(a);
1208
- adjacency.set(b, theirs);
1209
- };
1210
- for (let triangle = 0; triangle < owner.length; triangle++) {
1211
- const region = owner[triangle];
1212
- if (region === -1) continue;
1213
- const corners = [0, 1, 2].map((corner) => {
1214
- const at = triangle * 3 + corner;
1215
- return keyFor(position.getX(at), position.getY(at), position.getZ(at));
1216
- });
1217
- for (let corner = 0; corner < 3; corner++) {
1218
- const from = corners[corner];
1219
- const to = corners[(corner + 1) % 3];
1220
- const edge = from < to ? `${from}|${to}` : `${to}|${from}`;
1221
- const seen = edges.get(edge);
1222
- if (seen === void 0) edges.set(edge, region);
1223
- else join(seen, region);
1224
- }
1225
- }
1226
- return adjacency;
1227
- }
1228
-
1229
1777
  // src/render/selection.ts
1230
1778
  var FEATURE_TYPE_RANKS = [
1231
1779
  ["blind_hole", "through_hole", "filleted_blind_hole"],
@@ -1337,7 +1885,7 @@ import {
1337
1885
  useMemo as useMemo4,
1338
1886
  useRef as useRef3
1339
1887
  } from "react";
1340
- import { Box3 as Box33, Vector3 as Vector38 } from "three";
1888
+ import { Box3 as Box33, Vector3 as Vector39 } from "three";
1341
1889
 
1342
1890
  // src/camera.tsx
1343
1891
  import { useFrame, useThree as useThree2 } from "@react-three/fiber";
@@ -1353,9 +1901,9 @@ import {
1353
1901
  Raycaster,
1354
1902
  Sphere,
1355
1903
  Spherical,
1356
- Vector2 as Vector22,
1357
- Vector3 as Vector35,
1358
- Vector4
1904
+ Vector2 as Vector23,
1905
+ Vector3 as Vector36,
1906
+ Vector4 as Vector42
1359
1907
  } from "three";
1360
1908
 
1361
1909
  // src/render/control-schemes.ts
@@ -1451,9 +1999,9 @@ CameraControls.install({
1451
1999
  Raycaster,
1452
2000
  Sphere,
1453
2001
  Spherical,
1454
- Vector2: Vector22,
1455
- Vector3: Vector35,
1456
- Vector4
2002
+ Vector2: Vector23,
2003
+ Vector3: Vector36,
2004
+ Vector4: Vector42
1457
2005
  }
1458
2006
  });
1459
2007
  var EPSILON3 = 1e-6;
@@ -1475,9 +2023,9 @@ var ExtendedCameraControls = class extends CameraControls {
1475
2023
  // Scratch objects — `#onPointerMove` and `#adaptUpVector` run at pointer and
1476
2024
  // frame rate respectively, so neither may allocate.
1477
2025
  #spherical = new Spherical();
1478
- #scratchA = new Vector35();
1479
- #scratchB = new Vector35();
1480
- #scratchC = new Vector35();
2026
+ #scratchA = new Vector36();
2027
+ #scratchB = new Vector36();
2028
+ #scratchC = new Vector36();
1481
2029
  constructor(camera, domElement, options = {}) {
1482
2030
  super(camera, domElement);
1483
2031
  this.#domElement = domElement;
@@ -1819,10 +2367,10 @@ function counted(notify) {
1819
2367
  import { useFrame as useFrame2, useThree as useThree3 } from "@react-three/fiber";
1820
2368
  import { useEffect as useEffect3, useMemo as useMemo3, useRef as useRef2 } from "react";
1821
2369
  import {
1822
- DoubleSide as DoubleSide2,
2370
+ DoubleSide as DoubleSide3,
1823
2371
  RingGeometry,
1824
2372
  SphereGeometry,
1825
- Vector3 as Vector36
2373
+ Vector3 as Vector37
1826
2374
  } from "three";
1827
2375
 
1828
2376
  // src/render/target.ts
@@ -1865,8 +2413,8 @@ var TargetMarker = ({ controlsRef }) => {
1865
2413
  const heldUntil = useRef2(0);
1866
2414
  const painted = useRef2(-1);
1867
2415
  const placed = useRef2(false);
1868
- const lastTarget = useMemo3(() => new Vector36(), []);
1869
- const scratch = useMemo3(() => new Vector36(), []);
2416
+ const lastTarget = useMemo3(() => new Vector37(), []);
2417
+ const scratch = useMemo3(() => new Vector37(), []);
1870
2418
  useEffect3(() => {
1871
2419
  const controls = controlsRef.current;
1872
2420
  if (!controls) return void 0;
@@ -1932,7 +2480,7 @@ var TargetMarker = ({ controlsRef }) => {
1932
2480
  opacity: 0,
1933
2481
  depthTest: false,
1934
2482
  depthWrite: false,
1935
- side: DoubleSide2
2483
+ side: DoubleSide3
1936
2484
  }
1937
2485
  )
1938
2486
  }
@@ -1962,11 +2510,11 @@ var TargetMarker = ({ controlsRef }) => {
1962
2510
 
1963
2511
  // src/render/view-cube.ts
1964
2512
  import {
1965
- BufferGeometry,
2513
+ BufferGeometry as BufferGeometry2,
1966
2514
  CanvasTexture,
1967
- Float32BufferAttribute as Float32BufferAttribute3,
2515
+ Float32BufferAttribute as Float32BufferAttribute4,
1968
2516
  SRGBColorSpace,
1969
- Vector3 as Vector37
2517
+ Vector3 as Vector38
1970
2518
  } from "three";
1971
2519
  var VIEW_NAMES = [
1972
2520
  // The six faces.
@@ -2050,12 +2598,12 @@ function viewUp(direction) {
2050
2598
  return { x: 0, y: 0, z: 1 };
2051
2599
  }
2052
2600
  function squaredUp(direction, currentUp) {
2053
- const forward = new Vector37(direction.x, direction.y, direction.z).normalize();
2601
+ const forward = new Vector38(direction.x, direction.y, direction.z).normalize();
2054
2602
  const canonical = viewUp(direction);
2055
- const zero = new Vector37(canonical.x, canonical.y, canonical.z);
2603
+ const zero = new Vector38(canonical.x, canonical.y, canonical.z);
2056
2604
  zero.addScaledVector(forward, -forward.dot(zero)).normalize();
2057
- const quarter = new Vector37().crossVectors(forward, zero);
2058
- const current = new Vector37(currentUp.x, currentUp.y, currentUp.z);
2605
+ const quarter = new Vector38().crossVectors(forward, zero);
2606
+ const current = new Vector38(currentUp.x, currentUp.y, currentUp.z);
2059
2607
  const rolls = [zero, quarter, zero.clone().negate(), quarter.clone().negate()];
2060
2608
  const nearest = rolls.reduce(
2061
2609
  (best, roll) => roll.dot(current) > best.dot(current) ? roll : best
@@ -2073,11 +2621,11 @@ function cubeZones(chamfer = CHAMFER) {
2073
2621
  const direction = viewVector(name);
2074
2622
  const [sx, sy, sz] = VIEW_SIGNS[name];
2075
2623
  const sign = (axis) => VIEW_SIGNS[name][axis] ?? 0;
2076
- const normal = new Vector37(direction.x, direction.y, direction.z);
2624
+ const normal = new Vector38(direction.x, direction.y, direction.z);
2077
2625
  let polygon;
2078
2626
  if (kind === "face") {
2079
2627
  const up = viewUp(direction);
2080
- const upVector = new Vector37(up.x, up.y, up.z);
2628
+ const upVector = new Vector38(up.x, up.y, up.z);
2081
2629
  const right = normal.clone().negate().cross(upVector).normalize();
2082
2630
  const square = [
2083
2631
  [-1, -1],
@@ -2099,7 +2647,7 @@ function cubeZones(chamfer = CHAMFER) {
2099
2647
  [chamfer, 1, -chamfer]
2100
2648
  ];
2101
2649
  polygon = rectangle.map(
2102
- ([onA, onB, onC]) => new Vector37().setComponent(a, sign(a) * onA).setComponent(b, sign(b) * onB).setComponent(c, onC)
2650
+ ([onA, onB, onC]) => new Vector38().setComponent(a, sign(a) * onA).setComponent(b, sign(b) * onB).setComponent(c, onC)
2103
2651
  );
2104
2652
  } else {
2105
2653
  const triangle = [
@@ -2107,11 +2655,11 @@ function cubeZones(chamfer = CHAMFER) {
2107
2655
  [chamfer, 1, chamfer],
2108
2656
  [chamfer, chamfer, 1]
2109
2657
  ];
2110
- polygon = triangle.map(([onX, onY, onZ]) => new Vector37(sx * onX, sy * onY, sz * onZ));
2658
+ polygon = triangle.map(([onX, onY, onZ]) => new Vector38(sx * onX, sy * onY, sz * onZ));
2111
2659
  }
2112
2660
  const [first, second, third] = polygon;
2113
2661
  if (first && second && third) {
2114
- const facing = new Vector37().subVectors(second, first).cross(new Vector37().subVectors(third, first));
2662
+ const facing = new Vector38().subVectors(second, first).cross(new Vector38().subVectors(third, first));
2115
2663
  if (facing.dot(normal) < 0) polygon.reverse();
2116
2664
  }
2117
2665
  return { name, kind, direction, polygon: polygon.map(vec) };
@@ -2121,7 +2669,7 @@ function panelGeometry(zone) {
2121
2669
  const positions = [];
2122
2670
  const normals = [];
2123
2671
  const [first] = zone.polygon;
2124
- const geometry = new BufferGeometry();
2672
+ const geometry = new BufferGeometry2();
2125
2673
  if (!first) return geometry;
2126
2674
  for (let index = 1; index + 1 < zone.polygon.length; index += 1) {
2127
2675
  const b = zone.polygon[index];
@@ -2132,8 +2680,8 @@ function panelGeometry(zone) {
2132
2680
  normals.push(zone.direction.x, zone.direction.y, zone.direction.z);
2133
2681
  }
2134
2682
  }
2135
- geometry.setAttribute("position", new Float32BufferAttribute3(positions, 3));
2136
- geometry.setAttribute("normal", new Float32BufferAttribute3(normals, 3));
2683
+ geometry.setAttribute("position", new Float32BufferAttribute4(positions, 3));
2684
+ geometry.setAttribute("normal", new Float32BufferAttribute4(normals, 3));
2137
2685
  return geometry;
2138
2686
  }
2139
2687
  function cubeOutlineGeometry(zones) {
@@ -2146,8 +2694,8 @@ function cubeOutlineGeometry(zones) {
2146
2694
  positions.push(from.x, from.y, from.z, to.x, to.y, to.z);
2147
2695
  }
2148
2696
  }
2149
- const geometry = new BufferGeometry();
2150
- geometry.setAttribute("position", new Float32BufferAttribute3(positions, 3));
2697
+ const geometry = new BufferGeometry2();
2698
+ geometry.setAttribute("position", new Float32BufferAttribute4(positions, 3));
2151
2699
  return geometry;
2152
2700
  }
2153
2701
  function labelGeometry(zone) {
@@ -2171,9 +2719,9 @@ function labelGeometry(zone) {
2171
2719
  );
2172
2720
  uvs.push(uv[0] ?? 0, uv[1] ?? 0);
2173
2721
  }
2174
- const geometry = new BufferGeometry();
2175
- geometry.setAttribute("position", new Float32BufferAttribute3(positions, 3));
2176
- geometry.setAttribute("uv", new Float32BufferAttribute3(uvs, 2));
2722
+ const geometry = new BufferGeometry2();
2723
+ geometry.setAttribute("position", new Float32BufferAttribute4(positions, 3));
2724
+ geometry.setAttribute("uv", new Float32BufferAttribute4(uvs, 2));
2177
2725
  return geometry;
2178
2726
  }
2179
2727
  function labelTexture(label, color) {
@@ -2241,10 +2789,10 @@ var ViewerScene = ({
2241
2789
  const framedRef = useRef3(null);
2242
2790
  const scratchBox = useMemo4(() => new Box33(), []);
2243
2791
  const scratchBoundary = useMemo4(() => new Box33(), []);
2244
- const scratchDirection = useMemo4(() => new Vector38(), []);
2245
- const scratchView = useMemo4(() => new Vector38(), []);
2246
- const scratchTarget = useMemo4(() => new Vector38(), []);
2247
- const scratchPosition = useMemo4(() => new Vector38(), []);
2792
+ const scratchDirection = useMemo4(() => new Vector39(), []);
2793
+ const scratchView = useMemo4(() => new Vector39(), []);
2794
+ const scratchTarget = useMemo4(() => new Vector39(), []);
2795
+ const scratchPosition = useMemo4(() => new Vector39(), []);
2248
2796
  const applyLimits = useCallback(
2249
2797
  (bounds, framed) => {
2250
2798
  controlsRef.current?.applyLimits(
@@ -2293,8 +2841,8 @@ var ViewerScene = ({
2293
2841
  (bounds) => {
2294
2842
  const controls = controlsRef.current;
2295
2843
  if (!controls) return false;
2296
- const target = controls.getTarget(new Vector38());
2297
- const direction = currentViewDirection(camera, target, new Vector38());
2844
+ const target = controls.getTarget(new Vector39());
2845
+ const direction = currentViewDirection(camera, target, new Vector39());
2298
2846
  const distance = fitDistance(projection, size, bounds, DEFAULT_FIT_MARGIN);
2299
2847
  const position = direction.multiplyScalar(distance).add(bounds.center);
2300
2848
  void controls.setLookAt(
@@ -2319,8 +2867,8 @@ var ViewerScene = ({
2319
2867
  const fitContent = useCallback(() => {
2320
2868
  const controls = controlsRef.current;
2321
2869
  if (!controls) return false;
2322
- const target = controls.getTarget(new Vector38());
2323
- return frame(currentViewDirection(camera, target, new Vector38()), true);
2870
+ const target = controls.getTarget(new Vector39());
2871
+ return frame(currentViewDirection(camera, target, new Vector39()), true);
2324
2872
  }, [camera, frame]);
2325
2873
  const resetContent = useCallback(() => {
2326
2874
  const bounds = measure();
@@ -2526,67 +3074,234 @@ var Viewer = forwardRef(function Viewer2({
2526
3074
  });
2527
3075
 
2528
3076
  // src/section-view.tsx
2529
- import { useFrame as useFrame4, useThree as useThree5 } from "@react-three/fiber";
2530
- import { useCallback as useCallback2, useEffect as useEffect5, useLayoutEffect, useMemo as useMemo5, useRef as useRef4, useState } from "react";
3077
+ import { useFrame as useFrame4, useThree as useThree6 } from "@react-three/fiber";
3078
+ import { useCallback as useCallback2, useEffect as useEffect6, useLayoutEffect as useLayoutEffect2, useMemo as useMemo6, useRef as useRef4, useState } from "react";
2531
3079
  import {
2532
- Color as Color3,
2533
- DoubleSide as DoubleSide4,
3080
+ Color as Color5,
3081
+ DoubleSide as DoubleSide5,
2534
3082
  EdgesGeometry,
2535
- Mesh as Mesh3,
3083
+ Mesh as Mesh4,
2536
3084
  Plane as Plane2,
2537
3085
  PlaneGeometry,
2538
3086
  Quaternion as Quaternion2,
2539
3087
  Raycaster as Raycaster2,
2540
3088
  Scene,
2541
- Vector2 as Vector24,
2542
- Vector3 as Vector310
3089
+ Vector2 as Vector26,
3090
+ Vector3 as Vector311
2543
3091
  } from "three";
2544
3092
 
2545
- // src/render/directions.ts
2546
- import { Vector3 as Vector39 } from "three";
2547
- var CONE_AXIS = new Vector39(0, 1, 0);
2548
- var LENGTH = 0.45;
2549
- var HEAD = 0.45;
2550
- var HEAD_RADIUS = 0.3;
2551
- var SHAFT_RADIUS = 0.09;
2552
- var GAP = 0.2;
2553
- function arrowPlacement(direction, box) {
2554
- const center = box.getCenter(new Vector39());
2555
- const half = box.getSize(new Vector39()).multiplyScalar(0.5);
2556
- const radius = half.length() || 1;
2557
- const axis = new Vector39(direction.x, direction.y, direction.z);
2558
- if (axis.lengthSq() === 0) axis.set(0, 0, 1);
2559
- axis.normalize();
2560
- let exit = Number.POSITIVE_INFINITY;
2561
- for (const [component, extent] of [
2562
- [axis.x, half.x],
2563
- [axis.y, half.y],
2564
- [axis.z, half.z]
2565
- ]) {
2566
- if (Math.abs(component) > 1e-6) exit = Math.min(exit, extent / Math.abs(component));
2567
- }
2568
- if (!Number.isFinite(exit)) exit = radius;
2569
- return { tip: center.addScaledVector(axis, exit + radius * GAP), length: radius * LENGTH };
3093
+ // src/arrow-mesh.tsx
3094
+ import { useThree as useThree5 } from "@react-three/fiber";
3095
+ import { useEffect as useEffect5, useLayoutEffect, useMemo as useMemo5 } from "react";
3096
+
3097
+ // src/render/arrow-material.ts
3098
+ import { AlwaysDepth, Color as Color3, ShaderMaterial as ShaderMaterial2 } from "three";
3099
+ var VERTEX = (
3100
+ /* glsl */
3101
+ `
3102
+ varying vec3 vNormal;
3103
+
3104
+ void main() {
3105
+ vNormal = normalize(normalMatrix * normal);
3106
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
3107
+ }
3108
+ `
3109
+ );
3110
+ var FRAGMENT = (
3111
+ /* glsl */
3112
+ `
3113
+ uniform vec3 uColor;
3114
+ uniform float uOpacity;
3115
+
3116
+ varying vec3 vNormal;
3117
+
3118
+ // Above, to the left and toward the camera, in the camera's own frame.
3119
+ const vec3 KEY = vec3(-0.4, 0.6, 0.7);
3120
+
3121
+ void main() {
3122
+ // Wrapped, and over a narrow range: a quarter darker at most, so the shape
3123
+ // shows without the colour going anywhere.
3124
+ float light = mix(0.75, 1.0, 0.5 + 0.5 * dot(normalize(vNormal), normalize(KEY)));
3125
+ gl_FragColor = vec4(uColor * light, uOpacity);
3126
+ #include <tonemapping_fragment>
3127
+ #include <colorspace_fragment>
3128
+ }
3129
+ `
3130
+ );
3131
+ function createArrowMaterial() {
3132
+ const uniforms = {
3133
+ uColor: { value: new Color3() },
3134
+ uOpacity: { value: 1 }
3135
+ };
3136
+ return new ShaderMaterial2({
3137
+ vertexShader: VERTEX,
3138
+ fragmentShader: FRAGMENT,
3139
+ uniforms,
3140
+ transparent: true
3141
+ });
3142
+ }
3143
+ function createArrowDepthMaterial() {
3144
+ const material = createArrowMaterial();
3145
+ material.colorWrite = false;
3146
+ return material;
3147
+ }
3148
+ function applyArrowColor(material, color, opacity) {
3149
+ material.uniforms.uColor.value.set(color);
3150
+ material.uniforms.uOpacity.value = opacity;
3151
+ }
3152
+ var RESET_VERTEX = (
3153
+ /* glsl */
3154
+ `
3155
+ void main() {
3156
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
3157
+ // On the far plane, so what it leaves in the depth buffer is "nothing here".
3158
+ gl_Position.z = gl_Position.w;
3159
+ }
3160
+ `
3161
+ );
3162
+ var RESET_FRAGMENT = (
3163
+ /* glsl */
3164
+ `
3165
+ void main() {
3166
+ gl_FragColor = vec4(0.0);
3167
+ }
3168
+ `
3169
+ );
3170
+ function createDepthResetMaterial() {
3171
+ return new ShaderMaterial2({
3172
+ vertexShader: RESET_VERTEX,
3173
+ fragmentShader: RESET_FRAGMENT,
3174
+ transparent: true,
3175
+ colorWrite: false,
3176
+ depthFunc: AlwaysDepth
3177
+ });
3178
+ }
3179
+
3180
+ // src/arrow-mesh.tsx
3181
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
3182
+ var ArrowMesh = ({ geometry, color, opacity, onTop = false, events }) => {
3183
+ const invalidate = useThree5((state) => state.invalidate);
3184
+ const material = useMemo5(() => createArrowMaterial(), []);
3185
+ useEffect5(() => () => material.dispose(), [material]);
3186
+ const depth = useMemo5(() => createArrowDepthMaterial(), []);
3187
+ useEffect5(() => () => depth.dispose(), [depth]);
3188
+ const reset = useMemo5(() => onTop ? createDepthResetMaterial() : null, [onTop]);
3189
+ useEffect5(() => () => reset?.dispose(), [reset]);
3190
+ useLayoutEffect(() => {
3191
+ applyArrowColor(material, color, opacity);
3192
+ invalidate();
3193
+ }, [color, invalidate, material, opacity]);
3194
+ return /* @__PURE__ */ jsxs3(Fragment, { children: [
3195
+ reset ? /* @__PURE__ */ jsx4("mesh", { geometry, material: reset, renderOrder: 0, raycast: () => null }) : null,
3196
+ /* @__PURE__ */ jsx4("mesh", { geometry, material: depth, renderOrder: 1, raycast: () => null }),
3197
+ /* @__PURE__ */ jsx4("mesh", { geometry, material, renderOrder: 2, ...events })
3198
+ ] });
3199
+ };
3200
+
3201
+ // src/render/arrow.ts
3202
+ import { LatheGeometry, Vector2 as Vector24, Vector3 as Vector310 } from "three";
3203
+ var ARROW_AXIS2 = new Vector310(0, 1, 0);
3204
+ var ARROW_ROUND = 32;
3205
+ var HEAD_LENGTH = 0.415;
3206
+ var HEAD_RADIUS = 0.153;
3207
+ var SHAFT_RADIUS = 0.0515;
3208
+ var HANDLE_TIP = 0.76;
3209
+ var HANDLE_HEAD_LENGTH = 0.3;
3210
+ var HANDLE_HEAD_RADIUS = 0.14;
3211
+ var HANDLE_SHAFT_RADIUS = 0.035;
3212
+ var HANDLE_BEAD_RADIUS = 0.1;
3213
+ var RIM_ROUND = 6e-3;
3214
+ var NECK_ROUND = 8e-3;
3215
+ var BEAD_ROUND = 0.02;
3216
+ var ROUND_STEPS = 4;
3217
+ var CAP_STEPS = 6;
3218
+ var BEAD_STEPS = 12;
3219
+ var head = ({ length, radius, shaft }) => [
3220
+ { at: new Vector24(0, 0) },
3221
+ { at: new Vector24(radius, length), round: RIM_ROUND },
3222
+ { at: new Vector24(shaft, length), round: NECK_ROUND }
3223
+ ];
3224
+ var rounded = (corners) => corners.flatMap(({ at, round = 0 }, index) => {
3225
+ const before = corners[index - 1]?.at;
3226
+ const after = corners[index + 1]?.at;
3227
+ if (!(round > 0) || !before || !after) return [at];
3228
+ const back = before.clone().sub(at);
3229
+ const ahead = after.clone().sub(at);
3230
+ const reach = Math.min(back.length(), ahead.length()) / 2;
3231
+ back.normalize();
3232
+ ahead.normalize();
3233
+ const angle = Math.acos(Math.min(Math.max(back.dot(ahead), -1), 1));
3234
+ if (angle > Math.PI - 1e-6 || angle < 1e-6) return [at];
3235
+ const half = angle / 2;
3236
+ const offset = Math.min(round / Math.tan(half), reach);
3237
+ const radius = offset * Math.tan(half);
3238
+ const centre = at.clone().addScaledVector(back.clone().add(ahead).normalize(), radius / Math.sin(half));
3239
+ const from = at.clone().addScaledVector(back, offset).sub(centre);
3240
+ const to = at.clone().addScaledVector(ahead, offset).sub(centre);
3241
+ const start = Math.atan2(from.y, from.x);
3242
+ let sweep = Math.atan2(to.y, to.x) - start;
3243
+ if (sweep > Math.PI) sweep -= Math.PI * 2;
3244
+ if (sweep < -Math.PI) sweep += Math.PI * 2;
3245
+ return Array.from({ length: ROUND_STEPS + 1 }, (_, step) => {
3246
+ const turn = start + sweep * step / ROUND_STEPS;
3247
+ return new Vector24(centre.x + Math.cos(turn) * radius, centre.y + Math.sin(turn) * radius);
3248
+ });
3249
+ });
3250
+ function arrowProfile() {
3251
+ const body = rounded([
3252
+ ...head({ length: HEAD_LENGTH, radius: HEAD_RADIUS, shaft: SHAFT_RADIUS }),
3253
+ { at: new Vector24(SHAFT_RADIUS, 1 - SHAFT_RADIUS) }
3254
+ ]);
3255
+ const tail = Array.from({ length: CAP_STEPS }, (_, step) => {
3256
+ const turn = (step + 1) / CAP_STEPS * (Math.PI / 2);
3257
+ return new Vector24(
3258
+ step + 1 === CAP_STEPS ? 0 : Math.cos(turn) * SHAFT_RADIUS,
3259
+ 1 - SHAFT_RADIUS + Math.sin(turn) * SHAFT_RADIUS
3260
+ );
3261
+ });
3262
+ return [...body, ...tail];
3263
+ }
3264
+ function handleProfile() {
3265
+ const lower = head({
3266
+ length: HANDLE_HEAD_LENGTH,
3267
+ radius: HANDLE_HEAD_RADIUS,
3268
+ shaft: HANDLE_SHAFT_RADIUS
3269
+ }).map(({ at, round }) => ({ at: new Vector24(at.x, at.y - HANDLE_TIP), round }));
3270
+ const upper = lower.slice().reverse().map(({ at, round }) => ({ at: new Vector24(at.x, -at.y), round }));
3271
+ const meet = Math.acos(HANDLE_SHAFT_RADIUS / HANDLE_BEAD_RADIUS);
3272
+ const bead = Array.from({ length: BEAD_STEPS + 1 }, (_, step) => {
3273
+ const turn = -meet + 2 * meet * step / BEAD_STEPS;
3274
+ const at = new Vector24(Math.cos(turn), Math.sin(turn)).multiplyScalar(HANDLE_BEAD_RADIUS);
3275
+ return { at, round: step === 0 || step === BEAD_STEPS ? BEAD_ROUND : 0 };
3276
+ });
3277
+ return rounded([...lower, ...bead, ...upper]);
2570
3278
  }
3279
+ var turned = (profile) => {
3280
+ const geometry = new LatheGeometry(profile, ARROW_ROUND);
3281
+ geometry.normalizeNormals();
3282
+ return geometry;
3283
+ };
3284
+ var arrowGeometry = () => turned(arrowProfile());
3285
+ var handleGeometry = () => turned(handleProfile());
2571
3286
 
2572
3287
  // src/render/section-cap.ts
2573
3288
  import {
2574
3289
  AlwaysStencilFunc,
2575
3290
  BackSide,
2576
- Color as Color2,
3291
+ Color as Color4,
2577
3292
  DecrementWrapStencilOp,
2578
- DoubleSide as DoubleSide3,
3293
+ DoubleSide as DoubleSide4,
2579
3294
  FrontSide,
2580
3295
  IncrementWrapStencilOp,
2581
3296
  LinearFilter,
2582
- MeshBasicMaterial as MeshBasicMaterial2,
2583
- NotEqualStencilFunc,
2584
- ReplaceStencilOp,
2585
- ShaderMaterial,
2586
- Vector2 as Vector23,
3297
+ MeshBasicMaterial as MeshBasicMaterial3,
3298
+ NotEqualStencilFunc as NotEqualStencilFunc2,
3299
+ ReplaceStencilOp as ReplaceStencilOp2,
3300
+ ShaderMaterial as ShaderMaterial3,
3301
+ Vector2 as Vector25,
2587
3302
  WebGLRenderTarget
2588
3303
  } from "three";
2589
- var VERTEX = (
3304
+ var VERTEX2 = (
2590
3305
  /* glsl */
2591
3306
  `
2592
3307
  void main() {
@@ -2594,7 +3309,7 @@ void main() {
2594
3309
  }
2595
3310
  `
2596
3311
  );
2597
- var FRAGMENT = (
3312
+ var FRAGMENT2 = (
2598
3313
  /* glsl */
2599
3314
  `
2600
3315
  uniform sampler2D uMask;
@@ -2642,25 +3357,25 @@ function createMaskTarget() {
2642
3357
  function createCapMaterial(mask) {
2643
3358
  const uniforms = {
2644
3359
  uMask: { value: mask },
2645
- uResolution: { value: new Vector23(1, 1) },
2646
- uFill: { value: new Color2() },
2647
- uHatch: { value: new Color2() },
2648
- uOutline: { value: new Color2() },
3360
+ uResolution: { value: new Vector25(1, 1) },
3361
+ uFill: { value: new Color4() },
3362
+ uHatch: { value: new Color4() },
3363
+ uOutline: { value: new Color4() },
2649
3364
  uPitch: { value: CAP_HATCH.pitch },
2650
3365
  uLine: { value: CAP_HATCH.line },
2651
3366
  uThickness: { value: CAP_HATCH.outline }
2652
3367
  };
2653
- return new ShaderMaterial({
2654
- vertexShader: VERTEX,
2655
- fragmentShader: FRAGMENT,
3368
+ return new ShaderMaterial3({
3369
+ vertexShader: VERTEX2,
3370
+ fragmentShader: FRAGMENT2,
2656
3371
  uniforms,
2657
- side: DoubleSide3,
3372
+ side: DoubleSide4,
2658
3373
  stencilWrite: true,
2659
3374
  stencilRef: 0,
2660
- stencilFunc: NotEqualStencilFunc,
2661
- stencilFail: ReplaceStencilOp,
2662
- stencilZFail: ReplaceStencilOp,
2663
- stencilZPass: ReplaceStencilOp
3375
+ stencilFunc: NotEqualStencilFunc2,
3376
+ stencilFail: ReplaceStencilOp2,
3377
+ stencilZFail: ReplaceStencilOp2,
3378
+ stencilZPass: ReplaceStencilOp2
2664
3379
  });
2665
3380
  }
2666
3381
  function applyCapTheme(material, theme, dpr) {
@@ -2681,14 +3396,14 @@ function createStencilMaterials(clip) {
2681
3396
  clippingPlanes: clip
2682
3397
  };
2683
3398
  return [
2684
- new MeshBasicMaterial2({
3399
+ new MeshBasicMaterial3({
2685
3400
  ...shared,
2686
3401
  side: BackSide,
2687
3402
  stencilFail: IncrementWrapStencilOp,
2688
3403
  stencilZFail: IncrementWrapStencilOp,
2689
3404
  stencilZPass: IncrementWrapStencilOp
2690
3405
  }),
2691
- new MeshBasicMaterial2({
3406
+ new MeshBasicMaterial3({
2692
3407
  ...shared,
2693
3408
  side: FrontSide,
2694
3409
  stencilFail: DecrementWrapStencilOp,
@@ -2698,25 +3413,25 @@ function createStencilMaterials(clip) {
2698
3413
  ];
2699
3414
  }
2700
3415
  function createMaskMaterial() {
2701
- return new MeshBasicMaterial2({
3416
+ return new MeshBasicMaterial3({
2702
3417
  color: 16777215,
2703
- side: DoubleSide3,
3418
+ side: DoubleSide4,
2704
3419
  stencilWrite: true,
2705
3420
  stencilRef: 0,
2706
- stencilFunc: NotEqualStencilFunc,
2707
- stencilFail: ReplaceStencilOp,
2708
- stencilZFail: ReplaceStencilOp,
2709
- stencilZPass: ReplaceStencilOp
3421
+ stencilFunc: NotEqualStencilFunc2,
3422
+ stencilFail: ReplaceStencilOp2,
3423
+ stencilZFail: ReplaceStencilOp2,
3424
+ stencilZPass: ReplaceStencilOp2
2710
3425
  });
2711
3426
  }
2712
3427
 
2713
3428
  // src/section-view.tsx
2714
- import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
3429
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
2715
3430
  var FURNITURE2 = { [EXCLUDE_FROM_FRAME]: true };
2716
3431
  function resolveSectionPlane(options, box) {
2717
3432
  if (!options?.enabled || box.isEmpty()) return null;
2718
3433
  const normal = options.plane?.normal ?? options.normal ?? DEFAULT_SECTION_NORMAL;
2719
- const axis = new Vector310(normal.x, normal.y, normal.z);
3434
+ const axis = new Vector311(normal.x, normal.y, normal.z);
2720
3435
  if (axis.lengthSq() === 0) axis.set(0, 0, 1);
2721
3436
  axis.normalize();
2722
3437
  const bounds = sectionBounds(box, normal);
@@ -2737,7 +3452,7 @@ function resolveSectionPlane(options, box) {
2737
3452
  }
2738
3453
  };
2739
3454
  }
2740
- var Z = new Vector310(0, 0, 1);
3455
+ var Z = new Vector311(0, 0, 1);
2741
3456
  var SectionView = ({
2742
3457
  geometry,
2743
3458
  box,
@@ -2747,67 +3462,69 @@ var SectionView = ({
2747
3462
  showHandle,
2748
3463
  onDrag
2749
3464
  }) => {
2750
- const camera = useThree5((state) => state.camera);
2751
- const size = useThree5((state) => state.size);
2752
- const dpr = useThree5((state) => state.viewport.dpr);
2753
- const gl = useThree5((state) => state.gl);
2754
- const invalidate = useThree5((state) => state.invalidate);
2755
- const controls = useThree5((state) => state.controls);
2756
- const domElement = useThree5((state) => state.gl.domElement);
3465
+ const camera = useThree6((state) => state.camera);
3466
+ const size = useThree6((state) => state.size);
3467
+ const dpr = useThree6((state) => state.viewport.dpr);
3468
+ const gl = useThree6((state) => state.gl);
3469
+ const invalidate = useThree6((state) => state.invalidate);
3470
+ const controls = useThree6((state) => state.controls);
3471
+ const domElement = useThree6((state) => state.gl.domElement);
2757
3472
  const handleRef = useRef4(null);
2758
3473
  const [hovered, setHovered] = useState(false);
2759
3474
  const dragging = useRef4(null);
2760
- const span = useMemo5(() => box.getSize(new Vector310()).length(), [box]);
3475
+ const span = useMemo6(() => box.getSize(new Vector311()).length(), [box]);
2761
3476
  const frameSize = span * SECTION_GIZMO_FRAME_SCALE;
2762
- const directionColor2 = useMemo5(() => sectionDirectionColor(plane.normal), [plane]);
3477
+ const directionColor2 = useMemo6(() => sectionDirectionColor(plane.normal), [plane]);
2763
3478
  const resolvedHandleColor = sectionHandleColor(handleColor, directionColor2);
2764
- const clip = useMemo5(() => [plane], [plane]);
2765
- const capPosition = useMemo5(() => sectionAnchor(box, plane), [box, plane]);
2766
- const capQuaternion = useMemo5(() => new Quaternion2().setFromUnitVectors(Z, plane.normal), [plane]);
2767
- const handleQuaternion = useMemo5(
2768
- () => new Quaternion2().setFromUnitVectors(CONE_AXIS, plane.normal.clone().negate()),
3479
+ const clip = useMemo6(() => [plane], [plane]);
3480
+ const capPosition = useMemo6(() => sectionAnchor(box, plane), [box, plane]);
3481
+ const capQuaternion = useMemo6(() => new Quaternion2().setFromUnitVectors(Z, plane.normal), [plane]);
3482
+ const handleQuaternion = useMemo6(
3483
+ () => new Quaternion2().setFromUnitVectors(ARROW_AXIS2, plane.normal.clone().negate()),
2769
3484
  [plane]
2770
3485
  );
2771
- const frameSurface = useMemo5(() => new PlaneGeometry(frameSize, frameSize), [frameSize]);
2772
- useEffect5(() => () => frameSurface.dispose(), [frameSurface]);
2773
- const frameEdges = useMemo5(() => new EdgesGeometry(frameSurface), [frameSurface]);
2774
- useEffect5(() => () => frameEdges.dispose(), [frameEdges]);
2775
- const stencils = useMemo5(() => createStencilMaterials([]), []);
2776
- useEffect5(() => () => stencils.forEach((material) => material.dispose()), [stencils]);
2777
- useLayoutEffect(() => {
3486
+ const frameSurface = useMemo6(() => new PlaneGeometry(frameSize, frameSize), [frameSize]);
3487
+ useEffect6(() => () => frameSurface.dispose(), [frameSurface]);
3488
+ const frameEdges = useMemo6(() => new EdgesGeometry(frameSurface), [frameSurface]);
3489
+ useEffect6(() => () => frameEdges.dispose(), [frameEdges]);
3490
+ const handleShape = useMemo6(() => handleGeometry(), []);
3491
+ useEffect6(() => () => handleShape.dispose(), [handleShape]);
3492
+ const stencils = useMemo6(() => createStencilMaterials([]), []);
3493
+ useEffect6(() => () => stencils.forEach((material) => material.dispose()), [stencils]);
3494
+ useLayoutEffect2(() => {
2778
3495
  for (const material of stencils) material.clippingPlanes = clip;
2779
3496
  }, [clip, stencils]);
2780
- const mask = useMemo5(() => createMaskTarget(), []);
2781
- useEffect5(() => () => mask.dispose(), [mask]);
2782
- const cap = useMemo5(() => createCapMaterial(mask.texture), [mask]);
2783
- useEffect5(() => () => cap.dispose(), [cap]);
2784
- const quad = useMemo5(() => new PlaneGeometry(span * 1.5, span * 1.5), [span]);
2785
- useEffect5(() => () => quad.dispose(), [quad]);
2786
- const maskScene = useMemo5(() => {
3497
+ const mask = useMemo6(() => createMaskTarget(), []);
3498
+ useEffect6(() => () => mask.dispose(), [mask]);
3499
+ const cap = useMemo6(() => createCapMaterial(mask.texture), [mask]);
3500
+ useEffect6(() => () => cap.dispose(), [cap]);
3501
+ const quad = useMemo6(() => new PlaneGeometry(span * 1.5, span * 1.5), [span]);
3502
+ useEffect6(() => () => quad.dispose(), [quad]);
3503
+ const maskScene = useMemo6(() => {
2787
3504
  const scene = new Scene();
2788
3505
  const [back, front] = stencils;
2789
- const backMesh = new Mesh3(geometry, back);
3506
+ const backMesh = new Mesh4(geometry, back);
2790
3507
  backMesh.renderOrder = SECTION_RENDER_ORDER.stencil;
2791
- const frontMesh = new Mesh3(geometry, front);
3508
+ const frontMesh = new Mesh4(geometry, front);
2792
3509
  frontMesh.renderOrder = SECTION_RENDER_ORDER.stencil;
2793
- const maskMesh = new Mesh3(quad, createMaskMaterial());
3510
+ const maskMesh = new Mesh4(quad, createMaskMaterial());
2794
3511
  maskMesh.renderOrder = SECTION_RENDER_ORDER.cap;
2795
3512
  scene.add(backMesh, frontMesh, maskMesh);
2796
3513
  return { scene, maskMesh };
2797
3514
  }, [geometry, quad, stencils]);
2798
- useEffect5(() => () => maskScene.maskMesh.material.dispose(), [maskScene]);
2799
- useEffect5(() => {
3515
+ useEffect6(() => () => maskScene.maskMesh.material.dispose(), [maskScene]);
3516
+ useEffect6(() => {
2800
3517
  maskScene.maskMesh.position.copy(capPosition);
2801
3518
  maskScene.maskMesh.quaternion.copy(capQuaternion);
2802
3519
  }, [capPosition, capQuaternion, maskScene]);
2803
- useLayoutEffect(() => {
2804
- const buffer = gl.getDrawingBufferSize(new Vector24());
3520
+ useLayoutEffect2(() => {
3521
+ const buffer = gl.getDrawingBufferSize(new Vector26());
2805
3522
  mask.setSize(buffer.x, buffer.y);
2806
3523
  cap.uniforms.uResolution.value.copy(buffer);
2807
3524
  applyCapTheme(cap, theme, dpr);
2808
3525
  invalidate();
2809
3526
  }, [cap, dpr, gl, invalidate, mask, size, theme]);
2810
- const clearColor = useMemo5(() => new Color3(), []);
3527
+ const clearColor = useMemo6(() => new Color5(), []);
2811
3528
  useFrame4(({ gl: renderer, camera: eye }) => {
2812
3529
  const previousAlpha = renderer.getClearAlpha();
2813
3530
  renderer.getClearColor(clearColor);
@@ -2839,7 +3556,7 @@ var SectionView = ({
2839
3556
  event.stopPropagation();
2840
3557
  const view = camera.position.clone().sub(capPosition).normalize();
2841
3558
  const surface = dragPlane(plane.normal.clone(), view, capPosition);
2842
- const hit = event.ray.intersectPlane(surface, new Vector310());
3559
+ const hit = event.ray.intersectPlane(surface, new Vector311());
2843
3560
  const start = {
2844
3561
  plane: surface,
2845
3562
  from: hit ? hit.dot(plane.normal) : 0,
@@ -2848,7 +3565,7 @@ var SectionView = ({
2848
3565
  dragging.current = start;
2849
3566
  setControlsEnabled(false);
2850
3567
  const raycaster = new Raycaster2();
2851
- const pointer = new Vector24();
3568
+ const pointer = new Vector26();
2852
3569
  const move = (native) => {
2853
3570
  const rect = domElement.getBoundingClientRect();
2854
3571
  pointer.set(
@@ -2856,7 +3573,7 @@ var SectionView = ({
2856
3573
  -((native.clientY - rect.top) / rect.height) * 2 + 1
2857
3574
  );
2858
3575
  raycaster.setFromCamera(pointer, camera);
2859
- const point = raycaster.ray.intersectPlane(start.plane, new Vector310());
3576
+ const point = raycaster.ray.intersectPlane(start.plane, new Vector311());
2860
3577
  if (!point) return;
2861
3578
  onDrag(start.constant - (point.dot(plane.normal) - start.from));
2862
3579
  invalidate();
@@ -2888,8 +3605,8 @@ var SectionView = ({
2888
3605
  // Excluded from framing, all of it. The cap is a quad half again as wide as
2889
3606
  // the part's diagonal, so a Fit that measured it would frame the cut rather
2890
3607
  // than the part and leave the part a speck in the middle of it.
2891
- /* @__PURE__ */ jsxs3("group", { userData: FURNITURE2, children: [
2892
- /* @__PURE__ */ jsx4(
3608
+ /* @__PURE__ */ jsxs4("group", { userData: FURNITURE2, children: [
3609
+ /* @__PURE__ */ jsx5(
2893
3610
  "mesh",
2894
3611
  {
2895
3612
  geometry,
@@ -2898,7 +3615,7 @@ var SectionView = ({
2898
3615
  raycast: () => null
2899
3616
  }
2900
3617
  ),
2901
- /* @__PURE__ */ jsx4(
3618
+ /* @__PURE__ */ jsx5(
2902
3619
  "mesh",
2903
3620
  {
2904
3621
  geometry,
@@ -2907,7 +3624,7 @@ var SectionView = ({
2907
3624
  raycast: () => null
2908
3625
  }
2909
3626
  ),
2910
- /* @__PURE__ */ jsx4(
3627
+ /* @__PURE__ */ jsx5(
2911
3628
  "mesh",
2912
3629
  {
2913
3630
  geometry: quad,
@@ -2918,78 +3635,46 @@ var SectionView = ({
2918
3635
  raycast: () => null
2919
3636
  }
2920
3637
  ),
2921
- showHandle && onDrag ? /* @__PURE__ */ jsxs3(Fragment, { children: [
2922
- /* @__PURE__ */ jsxs3(
3638
+ showHandle && onDrag ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
3639
+ /* @__PURE__ */ jsxs4(
2923
3640
  "group",
2924
3641
  {
2925
3642
  position: capPosition,
2926
3643
  quaternion: capQuaternion,
2927
3644
  renderOrder: SECTION_RENDER_ORDER.outline,
2928
3645
  children: [
2929
- /* @__PURE__ */ jsx4("mesh", { geometry: frameSurface, raycast: () => null, children: /* @__PURE__ */ jsx4(
3646
+ /* @__PURE__ */ jsx5("mesh", { geometry: frameSurface, raycast: () => null, children: /* @__PURE__ */ jsx5(
2930
3647
  "meshBasicMaterial",
2931
3648
  {
2932
3649
  color: directionColor2,
2933
3650
  transparent: true,
2934
3651
  opacity: 0.18,
2935
- side: DoubleSide4,
3652
+ side: DoubleSide5,
2936
3653
  depthTest: false,
2937
3654
  depthWrite: false
2938
3655
  }
2939
3656
  ) }),
2940
- /* @__PURE__ */ jsx4("lineSegments", { geometry: frameEdges, raycast: () => null, children: /* @__PURE__ */ jsx4("lineBasicMaterial", { color: directionColor2, depthTest: false, depthWrite: false }) })
3657
+ /* @__PURE__ */ jsx5("lineSegments", { geometry: frameEdges, raycast: () => null, children: /* @__PURE__ */ jsx5("lineBasicMaterial", { color: directionColor2, depthTest: false, depthWrite: false }) })
2941
3658
  ]
2942
3659
  }
2943
3660
  ),
2944
- /* @__PURE__ */ jsxs3(
3661
+ /* @__PURE__ */ jsx5(
2945
3662
  "group",
2946
3663
  {
2947
3664
  ref: handleRef,
2948
3665
  position: capPosition,
2949
3666
  quaternion: handleQuaternion,
2950
3667
  renderOrder: SECTION_RENDER_ORDER.handle,
2951
- children: [
2952
- /* @__PURE__ */ jsxs3("mesh", { ...grab, children: [
2953
- /* @__PURE__ */ jsx4("sphereGeometry", { args: [0.1, 16, 12] }),
2954
- /* @__PURE__ */ jsx4(
2955
- "meshBasicMaterial",
2956
- {
2957
- color: hovered ? theme.hover : resolvedHandleColor,
2958
- depthTest: false
2959
- }
2960
- )
2961
- ] }),
2962
- /* @__PURE__ */ jsxs3("mesh", { ...grab, children: [
2963
- /* @__PURE__ */ jsx4("cylinderGeometry", { args: [0.035, 0.035, 0.9, 12] }),
2964
- /* @__PURE__ */ jsx4(
2965
- "meshBasicMaterial",
2966
- {
2967
- color: hovered ? theme.hover : resolvedHandleColor,
2968
- depthTest: false
2969
- }
2970
- )
2971
- ] }),
2972
- /* @__PURE__ */ jsxs3("mesh", { position: [0, 0.61, 0], ...grab, children: [
2973
- /* @__PURE__ */ jsx4("coneGeometry", { args: [0.15, 0.3, 20] }),
2974
- /* @__PURE__ */ jsx4(
2975
- "meshBasicMaterial",
2976
- {
2977
- color: hovered ? theme.hover : resolvedHandleColor,
2978
- depthTest: false
2979
- }
2980
- )
2981
- ] }),
2982
- /* @__PURE__ */ jsxs3("mesh", { position: [0, -0.61, 0], rotation: [Math.PI, 0, 0], ...grab, children: [
2983
- /* @__PURE__ */ jsx4("coneGeometry", { args: [0.15, 0.3, 20] }),
2984
- /* @__PURE__ */ jsx4(
2985
- "meshBasicMaterial",
2986
- {
2987
- color: hovered ? theme.hover : resolvedHandleColor,
2988
- depthTest: false
2989
- }
2990
- )
2991
- ] })
2992
- ]
3668
+ children: /* @__PURE__ */ jsx5(
3669
+ ArrowMesh,
3670
+ {
3671
+ geometry: handleShape,
3672
+ color: hovered ? theme.hover : resolvedHandleColor,
3673
+ opacity: 1,
3674
+ onTop: true,
3675
+ events: grab
3676
+ }
3677
+ )
2993
3678
  }
2994
3679
  )
2995
3680
  ] }) : null
@@ -2998,11 +3683,11 @@ var SectionView = ({
2998
3683
  };
2999
3684
 
3000
3685
  // src/content-box.ts
3001
- import { useFrame as useFrame5, useThree as useThree6 } from "@react-three/fiber";
3686
+ import { useFrame as useFrame5, useThree as useThree7 } from "@react-three/fiber";
3002
3687
  import { useRef as useRef5, useState as useState2 } from "react";
3003
3688
  import { Box3 as Box34 } from "three";
3004
3689
  function useContentBox() {
3005
- const scene = useThree6((state) => state.scene);
3690
+ const scene = useThree7((state) => state.scene);
3006
3691
  const [box, setBox] = useState2(() => new Box34());
3007
3692
  const measured = useRef5(false);
3008
3693
  useFrame5(() => {
@@ -3017,7 +3702,7 @@ function useContentBox() {
3017
3702
  }
3018
3703
 
3019
3704
  // src/part-mesh.tsx
3020
- import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
3705
+ import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
3021
3706
  var PartMesh = ({
3022
3707
  model,
3023
3708
  geometry,
@@ -3038,17 +3723,19 @@ var PartMesh = ({
3038
3723
  onPick,
3039
3724
  theme,
3040
3725
  showEdges = true,
3726
+ sharpCorners = null,
3041
3727
  display = "solid",
3042
3728
  children
3043
3729
  }) => {
3044
- const { camera, controls, invalidate } = useThree7();
3045
- const context = useMemo6(() => ({ model, geometry }), [model, geometry]);
3730
+ const { camera, controls, invalidate, size } = useThree8();
3731
+ const context = useMemo7(() => ({ model, geometry }), [model, geometry]);
3046
3732
  const viewerControls = useViewerControls();
3047
3733
  const resolved = useStableTheme(theme);
3048
3734
  const currentTheme = useRef6(resolved);
3049
3735
  currentTheme.current = resolved;
3050
- const part = useMemo6(() => createPart(model, geometry, currentTheme.current), [geometry, model]);
3736
+ const part = useMemo7(() => createPart(model, geometry, currentTheme.current), [geometry, model]);
3051
3737
  const hoverRegion = useRef6(null);
3738
+ const hoverCorner = useRef6(null);
3052
3739
  const onHoverRef = useRef6(onHover);
3053
3740
  onHoverRef.current = onHover;
3054
3741
  const box = useContentBox();
@@ -3056,7 +3743,7 @@ var PartMesh = ({
3056
3743
  const shared = useSyncExternalStore(store.subscribe, store.get, store.get);
3057
3744
  const controlled = section !== void 0;
3058
3745
  const options = controlled ? section : shared ?? void 0;
3059
- const cut = useMemo6(() => resolveSectionPlane(options, box), [box, options]);
3746
+ const cut = useMemo7(() => resolveSectionPlane(options, box), [box, options]);
3060
3747
  const engaged = useSyncExternalStore(store.subscribe, store.isEngaged, store.isEngaged);
3061
3748
  const pressedWhileEngaged = useRef6(false);
3062
3749
  const layers = useRef6({
@@ -3079,24 +3766,37 @@ var PartMesh = ({
3079
3766
  const { hoveredFeatureIds: hoveredFeatures, ...rest } = layers.current;
3080
3767
  applyHighlightLayers(
3081
3768
  part,
3082
- { ...rest, hoveredFeatures, hoverRegion: hoverRegion.current },
3769
+ {
3770
+ ...rest,
3771
+ hoveredFeatures,
3772
+ // On a corner the corner is what is hovered, in the hover colour; the
3773
+ // face it lies on is not painted as if the pointer meant it.
3774
+ hoverRegion: hoverCorner.current === null ? hoverRegion.current : null
3775
+ },
3083
3776
  currentTheme.current
3084
3777
  );
3085
3778
  invalidate();
3086
3779
  }, [invalidate, part]);
3087
- useEffect6(() => () => part.dispose(), [part]);
3088
- const adjacency = useMemo6(() => regionAdjacency(model, geometry), [model, geometry]);
3089
- useEffect6(() => {
3780
+ const dropHover = useCallback3(() => {
3781
+ hoverRegion.current = null;
3782
+ hoverCorner.current = null;
3783
+ part.setHoveredSharpCorner(null);
3784
+ repaint();
3785
+ onHoverRef.current?.(null);
3786
+ }, [part, repaint]);
3787
+ useEffect7(() => () => part.dispose(), [part]);
3788
+ const adjacency = useMemo7(() => regionAdjacency(model, geometry), [model, geometry]);
3789
+ useEffect7(() => {
3090
3790
  onAdjacency?.(adjacency);
3091
3791
  }, [adjacency, onAdjacency]);
3092
3792
  const framed = useRef6(null);
3093
- useEffect6(() => {
3793
+ useEffect7(() => {
3094
3794
  if (focusFeature === null || focusFeature === framed.current) return;
3095
3795
  framed.current = focusFeature;
3096
3796
  const box2 = part.boxForFeature(focusFeature);
3097
3797
  if (box2) viewerControls.frameBox(box2);
3098
3798
  }, [focusFeature, part, viewerControls]);
3099
- useLayoutEffect2(() => {
3799
+ useLayoutEffect3(() => {
3100
3800
  part.setTheme(resolved);
3101
3801
  repaint();
3102
3802
  }, [part, repaint, resolved]);
@@ -3105,20 +3805,28 @@ var PartMesh = ({
3105
3805
  const selectionRef = useRef6(selection);
3106
3806
  selectionRef.current = selection;
3107
3807
  const focusKey = focusStateKey(focus, selection);
3108
- useLayoutEffect2(() => {
3808
+ useLayoutEffect3(() => {
3109
3809
  part.setFocus(selectionRef.current, focusRef.current);
3110
3810
  invalidate();
3111
3811
  }, [focusKey, invalidate, part]);
3112
- useLayoutEffect2(() => {
3812
+ useLayoutEffect3(() => {
3113
3813
  part.setDisplay(display, showEdges);
3114
3814
  invalidate();
3115
3815
  }, [display, invalidate, part, showEdges]);
3116
- useLayoutEffect2(() => {
3816
+ useLayoutEffect3(() => {
3117
3817
  part.setClippingPlanes(cut ? [cut.plane] : null);
3118
3818
  invalidate();
3119
3819
  }, [cut, invalidate, part]);
3820
+ const sharpCornersRef = useRef6(sharpCorners);
3821
+ sharpCornersRef.current = sharpCorners;
3822
+ const sharpKey = sharpCorners ? sharpCornerKey(sharpCorners) : "";
3823
+ useLayoutEffect3(() => {
3824
+ part.setSharpCorners(sharpCornersRef.current);
3825
+ if (hoverCorner.current !== null) dropHover();
3826
+ invalidate();
3827
+ }, [dropHover, invalidate, part, sharpKey]);
3120
3828
  const reportedSection = useRef6("");
3121
- useLayoutEffect2(() => {
3829
+ useLayoutEffect3(() => {
3122
3830
  const state = cut?.state ?? null;
3123
3831
  const key = state ? `${state.constant}|${state.normal.x},${state.normal.y},${state.normal.z}` : "";
3124
3832
  if (key === reportedSection.current) return;
@@ -3135,12 +3843,12 @@ var PartMesh = ({
3135
3843
  highlights.map((entry) => `${entry.tag}:${entry.color}:${entry.weight ?? ""}`).join(" "),
3136
3844
  regionHighlights.map((entry) => `${entry.region}:${entry.color}:${entry.weight ?? ""}`).join(" ")
3137
3845
  ].join("|");
3138
- useLayoutEffect2(() => {
3846
+ useLayoutEffect3(() => {
3139
3847
  repaint();
3140
3848
  }, [layerKey, repaint]);
3141
3849
  const isTap = useTapGuard();
3142
3850
  const retarget = useRetarget();
3143
- const doubles = useMemo6(() => trackDoubleTaps(), []);
3851
+ const doubles = useMemo7(() => trackDoubleTaps(), []);
3144
3852
  const pickFor = (event, doubled = false) => {
3145
3853
  const triangleIndex = event.faceIndex;
3146
3854
  if (triangleIndex == null) return null;
@@ -3149,7 +3857,7 @@ var PartMesh = ({
3149
3857
  const target = readTarget(controls);
3150
3858
  const normal = event.face?.normal ?? UP;
3151
3859
  const source = event.nativeEvent;
3152
- return buildPick({
3860
+ const pick = buildPick({
3153
3861
  model,
3154
3862
  region,
3155
3863
  triangleIndex,
@@ -3167,26 +3875,27 @@ var PartMesh = ({
3167
3875
  secondary: "button" in source && source.button === 2
3168
3876
  }
3169
3877
  });
3878
+ const corner = part.sharpCornerAt({ point: event.point, region }, camera, size);
3879
+ return corner ? { ...pick, sharpCorner: corner } : pick;
3170
3880
  };
3171
3881
  const emitHover = (next) => {
3172
3882
  const region = next?.region ?? null;
3173
- if (hoverRegion.current === region) return;
3883
+ const corner = next?.sharpCorner?.index ?? null;
3884
+ if (hoverRegion.current === region && hoverCorner.current === corner) return;
3174
3885
  hoverRegion.current = region;
3886
+ hoverCorner.current = corner;
3887
+ part.setHoveredSharpCorner(corner);
3175
3888
  repaint();
3176
3889
  onHover?.(next);
3177
3890
  };
3178
- useLayoutEffect2(() => {
3891
+ useLayoutEffect3(() => {
3179
3892
  if (hover || hoverRegion.current === null) return;
3180
- hoverRegion.current = null;
3181
- repaint();
3182
- onHoverRef.current?.(null);
3183
- }, [hover, repaint]);
3184
- useLayoutEffect2(() => {
3893
+ dropHover();
3894
+ }, [dropHover, hover]);
3895
+ useLayoutEffect3(() => {
3185
3896
  if (!engaged || hoverRegion.current === null) return;
3186
- hoverRegion.current = null;
3187
- repaint();
3188
- onHoverRef.current?.(null);
3189
- }, [engaged, repaint]);
3897
+ dropHover();
3898
+ }, [dropHover, engaged]);
3190
3899
  const dragSection = useCallback3(
3191
3900
  (constant) => {
3192
3901
  if (!cut) return;
@@ -3205,8 +3914,8 @@ var PartMesh = ({
3205
3914
  },
3206
3915
  [box, controlled, cut, onSectionChange, options, store]
3207
3916
  );
3208
- return /* @__PURE__ */ jsxs4(Fragment2, { children: [
3209
- /* @__PURE__ */ jsx5(
3917
+ return /* @__PURE__ */ jsxs5(Fragment3, { children: [
3918
+ /* @__PURE__ */ jsx6(
3210
3919
  "primitive",
3211
3920
  {
3212
3921
  object: part.object,
@@ -3243,7 +3952,7 @@ var PartMesh = ({
3243
3952
  }
3244
3953
  }
3245
3954
  ),
3246
- cut ? /* @__PURE__ */ jsx5(
3955
+ cut ? /* @__PURE__ */ jsx6(
3247
3956
  SectionView,
3248
3957
  {
3249
3958
  geometry,
@@ -3255,12 +3964,12 @@ var PartMesh = ({
3255
3964
  onDrag: !controlled || onSectionChange ? dragSection : void 0
3256
3965
  }
3257
3966
  ) : null,
3258
- children ? /* @__PURE__ */ jsx5(PartContext.Provider, { value: context, children }) : null
3967
+ children ? /* @__PURE__ */ jsx6(PartContext.Provider, { value: context, children }) : null
3259
3968
  ] });
3260
3969
  };
3261
- var ORIGIN = new Vector311();
3262
- var UP = new Vector311(0, 0, 1);
3263
- var TARGET = new Vector311();
3970
+ var ORIGIN = new Vector312();
3971
+ var UP = new Vector312(0, 0, 1);
3972
+ var TARGET = new Vector312();
3264
3973
  function readTarget(controls) {
3265
3974
  if (controls && typeof controls.getTarget === "function") {
3266
3975
  return controls.getTarget(TARGET);
@@ -3276,7 +3985,7 @@ function useStableTheme(theme) {
3276
3985
  }
3277
3986
 
3278
3987
  // src/engine/geometry.ts
3279
- import { Matrix4 as Matrix42, Mesh as Mesh4 } from "three";
3988
+ import { Matrix4 as Matrix42, Mesh as Mesh5 } from "three";
3280
3989
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3281
3990
  import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
3282
3991
  var PartMeshError = class extends Error {
@@ -3351,7 +4060,7 @@ function parseStl(data, mesh) {
3351
4060
  function singleMesh(scene) {
3352
4061
  const meshes = [];
3353
4062
  scene.traverse((object) => {
3354
- if (object instanceof Mesh4) meshes.push(object);
4063
+ if (object instanceof Mesh5) meshes.push(object);
3355
4064
  });
3356
4065
  const [mesh] = meshes;
3357
4066
  if (mesh === void 0 || meshes.length !== 1) {
@@ -3387,7 +4096,7 @@ function assertCounts(geometry, expected) {
3387
4096
  }
3388
4097
  function disposeMaterials(scene) {
3389
4098
  scene.traverse((object) => {
3390
- if (!(object instanceof Mesh4)) return;
4099
+ if (!(object instanceof Mesh5)) return;
3391
4100
  for (const material of Array.isArray(object.material) ? object.material : [object.material]) {
3392
4101
  material.dispose();
3393
4102
  }
@@ -3395,7 +4104,7 @@ function disposeMaterials(scene) {
3395
4104
  }
3396
4105
 
3397
4106
  // src/engine/normals.ts
3398
- import { Float32BufferAttribute as Float32BufferAttribute4 } from "three";
4107
+ import { Float32BufferAttribute as Float32BufferAttribute5 } from "three";
3399
4108
  function smoothRegionNormals(geometry, regions) {
3400
4109
  const position = geometry.getAttribute("position");
3401
4110
  if (!position || geometry.index) return;
@@ -3461,7 +4170,7 @@ function smoothRegionNormals(geometry, regions) {
3461
4170
  normals[vertex * 3 + 1] = y / length;
3462
4171
  normals[vertex * 3 + 2] = z / length;
3463
4172
  }
3464
- geometry.setAttribute("normal", new Float32BufferAttribute4(normals, 3));
4173
+ geometry.setAttribute("normal", new Float32BufferAttribute5(normals, 3));
3465
4174
  }
3466
4175
 
3467
4176
  // src/engine/geometry-cache.ts
@@ -3884,15 +4593,15 @@ function compareVersions(a, b) {
3884
4593
  }
3885
4594
 
3886
4595
  // src/engine/engine-part.tsx
3887
- import { jsx as jsx6 } from "react/jsx-runtime";
4596
+ import { jsx as jsx7 } from "react/jsx-runtime";
3888
4597
  var EnginePart = ({ report, ...props }) => {
3889
- const model = useMemo7(() => normalizePartReport(report), [report]);
4598
+ const model = useMemo8(() => normalizePartReport(report), [report]);
3890
4599
  const resource = useGeometryResource(model);
3891
- useEffect7(() => {
4600
+ useEffect8(() => {
3892
4601
  engineGeometryCache.retain(resource);
3893
4602
  return () => engineGeometryCache.release(resource);
3894
4603
  }, [resource]);
3895
- return /* @__PURE__ */ jsx6(PartMesh, { model, geometry: resource.geometry, ...props });
4604
+ return /* @__PURE__ */ jsx7(PartMesh, { model, geometry: resource.geometry, ...props });
3896
4605
  };
3897
4606
  function useGeometryResource(part) {
3898
4607
  const resource = engineGeometryCache.get(part);
@@ -4001,11 +4710,14 @@ export {
4001
4710
  useTapGuard,
4002
4711
  visualSurfaces,
4003
4712
  regionEdgesGeometry,
4713
+ regionAdjacency,
4714
+ surfaceCreases,
4715
+ findSharpCorners,
4716
+ sharpCornerPositions,
4004
4717
  REGION_ATTRIBUTE,
4005
4718
  buildRegionTexels,
4006
4719
  buildRegionAttribute,
4007
4720
  createPart,
4008
- regionAdjacency,
4009
4721
  FEATURE_TYPE_RANKS,
4010
4722
  featureTypeRank,
4011
4723
  rankOwners,
@@ -4045,11 +4757,9 @@ export {
4045
4757
  useSectionStore,
4046
4758
  useRetarget,
4047
4759
  Viewer,
4048
- CONE_AXIS,
4049
- HEAD,
4050
- HEAD_RADIUS,
4051
- SHAFT_RADIUS,
4052
- arrowPlacement,
4760
+ ArrowMesh,
4761
+ ARROW_AXIS2 as ARROW_AXIS,
4762
+ arrowGeometry,
4053
4763
  createMaskTarget,
4054
4764
  createCapMaterial,
4055
4765
  applyCapTheme,