@toolpath/viewer 2.2.1 → 2.3.1

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.
@@ -11,7 +11,7 @@ import {
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();
@@ -2529,17 +3077,17 @@ var Viewer = forwardRef(function Viewer2({
2529
3077
  import { useFrame as useFrame4, useThree as useThree6 } from "@react-three/fiber";
2530
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 Color4,
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 Vector25,
2542
- Vector3 as Vector310
3089
+ Vector2 as Vector26,
3090
+ Vector3 as Vector311
2543
3091
  } from "three";
2544
3092
 
2545
3093
  // src/arrow-mesh.tsx
@@ -2547,7 +3095,7 @@ import { useThree as useThree5 } from "@react-three/fiber";
2547
3095
  import { useEffect as useEffect5, useLayoutEffect, useMemo as useMemo5 } from "react";
2548
3096
 
2549
3097
  // src/render/arrow-material.ts
2550
- import { AlwaysDepth, Color as Color2, ShaderMaterial } from "three";
3098
+ import { AlwaysDepth, Color as Color3, ShaderMaterial as ShaderMaterial2 } from "three";
2551
3099
  var VERTEX = (
2552
3100
  /* glsl */
2553
3101
  `
@@ -2582,10 +3130,10 @@ void main() {
2582
3130
  );
2583
3131
  function createArrowMaterial() {
2584
3132
  const uniforms = {
2585
- uColor: { value: new Color2() },
3133
+ uColor: { value: new Color3() },
2586
3134
  uOpacity: { value: 1 }
2587
3135
  };
2588
- return new ShaderMaterial({
3136
+ return new ShaderMaterial2({
2589
3137
  vertexShader: VERTEX,
2590
3138
  fragmentShader: FRAGMENT,
2591
3139
  uniforms,
@@ -2620,7 +3168,7 @@ void main() {
2620
3168
  `
2621
3169
  );
2622
3170
  function createDepthResetMaterial() {
2623
- return new ShaderMaterial({
3171
+ return new ShaderMaterial2({
2624
3172
  vertexShader: RESET_VERTEX,
2625
3173
  fragmentShader: RESET_FRAGMENT,
2626
3174
  transparent: true,
@@ -2651,8 +3199,8 @@ var ArrowMesh = ({ geometry, color, opacity, onTop = false, events }) => {
2651
3199
  };
2652
3200
 
2653
3201
  // src/render/arrow.ts
2654
- import { LatheGeometry, Vector2 as Vector23, Vector3 as Vector39 } from "three";
2655
- var ARROW_AXIS2 = new Vector39(0, 1, 0);
3202
+ import { LatheGeometry, Vector2 as Vector24, Vector3 as Vector310 } from "three";
3203
+ var ARROW_AXIS2 = new Vector310(0, 1, 0);
2656
3204
  var ARROW_ROUND = 32;
2657
3205
  var HEAD_LENGTH = 0.415;
2658
3206
  var HEAD_RADIUS = 0.153;
@@ -2669,9 +3217,9 @@ var ROUND_STEPS = 4;
2669
3217
  var CAP_STEPS = 6;
2670
3218
  var BEAD_STEPS = 12;
2671
3219
  var head = ({ length, radius, shaft }) => [
2672
- { at: new Vector23(0, 0) },
2673
- { at: new Vector23(radius, length), round: RIM_ROUND },
2674
- { at: new Vector23(shaft, length), round: NECK_ROUND }
3220
+ { at: new Vector24(0, 0) },
3221
+ { at: new Vector24(radius, length), round: RIM_ROUND },
3222
+ { at: new Vector24(shaft, length), round: NECK_ROUND }
2675
3223
  ];
2676
3224
  var rounded = (corners) => corners.flatMap(({ at, round = 0 }, index) => {
2677
3225
  const before = corners[index - 1]?.at;
@@ -2696,17 +3244,17 @@ var rounded = (corners) => corners.flatMap(({ at, round = 0 }, index) => {
2696
3244
  if (sweep < -Math.PI) sweep += Math.PI * 2;
2697
3245
  return Array.from({ length: ROUND_STEPS + 1 }, (_, step) => {
2698
3246
  const turn = start + sweep * step / ROUND_STEPS;
2699
- return new Vector23(centre.x + Math.cos(turn) * radius, centre.y + Math.sin(turn) * radius);
3247
+ return new Vector24(centre.x + Math.cos(turn) * radius, centre.y + Math.sin(turn) * radius);
2700
3248
  });
2701
3249
  });
2702
3250
  function arrowProfile() {
2703
3251
  const body = rounded([
2704
3252
  ...head({ length: HEAD_LENGTH, radius: HEAD_RADIUS, shaft: SHAFT_RADIUS }),
2705
- { at: new Vector23(SHAFT_RADIUS, 1 - SHAFT_RADIUS) }
3253
+ { at: new Vector24(SHAFT_RADIUS, 1 - SHAFT_RADIUS) }
2706
3254
  ]);
2707
3255
  const tail = Array.from({ length: CAP_STEPS }, (_, step) => {
2708
3256
  const turn = (step + 1) / CAP_STEPS * (Math.PI / 2);
2709
- return new Vector23(
3257
+ return new Vector24(
2710
3258
  step + 1 === CAP_STEPS ? 0 : Math.cos(turn) * SHAFT_RADIUS,
2711
3259
  1 - SHAFT_RADIUS + Math.sin(turn) * SHAFT_RADIUS
2712
3260
  );
@@ -2718,12 +3266,12 @@ function handleProfile() {
2718
3266
  length: HANDLE_HEAD_LENGTH,
2719
3267
  radius: HANDLE_HEAD_RADIUS,
2720
3268
  shaft: HANDLE_SHAFT_RADIUS
2721
- }).map(({ at, round }) => ({ at: new Vector23(at.x, at.y - HANDLE_TIP), round }));
2722
- const upper = lower.slice().reverse().map(({ at, round }) => ({ at: new Vector23(at.x, -at.y), round }));
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 }));
2723
3271
  const meet = Math.acos(HANDLE_SHAFT_RADIUS / HANDLE_BEAD_RADIUS);
2724
3272
  const bead = Array.from({ length: BEAD_STEPS + 1 }, (_, step) => {
2725
3273
  const turn = -meet + 2 * meet * step / BEAD_STEPS;
2726
- const at = new Vector23(Math.cos(turn), Math.sin(turn)).multiplyScalar(HANDLE_BEAD_RADIUS);
3274
+ const at = new Vector24(Math.cos(turn), Math.sin(turn)).multiplyScalar(HANDLE_BEAD_RADIUS);
2727
3275
  return { at, round: step === 0 || step === BEAD_STEPS ? BEAD_ROUND : 0 };
2728
3276
  });
2729
3277
  return rounded([...lower, ...bead, ...upper]);
@@ -2740,17 +3288,17 @@ var handleGeometry = () => turned(handleProfile());
2740
3288
  import {
2741
3289
  AlwaysStencilFunc,
2742
3290
  BackSide,
2743
- Color as Color3,
3291
+ Color as Color4,
2744
3292
  DecrementWrapStencilOp,
2745
- DoubleSide as DoubleSide3,
3293
+ DoubleSide as DoubleSide4,
2746
3294
  FrontSide,
2747
3295
  IncrementWrapStencilOp,
2748
3296
  LinearFilter,
2749
- MeshBasicMaterial as MeshBasicMaterial2,
2750
- NotEqualStencilFunc,
2751
- ReplaceStencilOp,
2752
- ShaderMaterial as ShaderMaterial2,
2753
- Vector2 as Vector24,
3297
+ MeshBasicMaterial as MeshBasicMaterial3,
3298
+ NotEqualStencilFunc as NotEqualStencilFunc2,
3299
+ ReplaceStencilOp as ReplaceStencilOp2,
3300
+ ShaderMaterial as ShaderMaterial3,
3301
+ Vector2 as Vector25,
2754
3302
  WebGLRenderTarget
2755
3303
  } from "three";
2756
3304
  var VERTEX2 = (
@@ -2809,25 +3357,25 @@ function createMaskTarget() {
2809
3357
  function createCapMaterial(mask) {
2810
3358
  const uniforms = {
2811
3359
  uMask: { value: mask },
2812
- uResolution: { value: new Vector24(1, 1) },
2813
- uFill: { value: new Color3() },
2814
- uHatch: { value: new Color3() },
2815
- uOutline: { value: new Color3() },
3360
+ uResolution: { value: new Vector25(1, 1) },
3361
+ uFill: { value: new Color4() },
3362
+ uHatch: { value: new Color4() },
3363
+ uOutline: { value: new Color4() },
2816
3364
  uPitch: { value: CAP_HATCH.pitch },
2817
3365
  uLine: { value: CAP_HATCH.line },
2818
3366
  uThickness: { value: CAP_HATCH.outline }
2819
3367
  };
2820
- return new ShaderMaterial2({
3368
+ return new ShaderMaterial3({
2821
3369
  vertexShader: VERTEX2,
2822
3370
  fragmentShader: FRAGMENT2,
2823
3371
  uniforms,
2824
- side: DoubleSide3,
3372
+ side: DoubleSide4,
2825
3373
  stencilWrite: true,
2826
3374
  stencilRef: 0,
2827
- stencilFunc: NotEqualStencilFunc,
2828
- stencilFail: ReplaceStencilOp,
2829
- stencilZFail: ReplaceStencilOp,
2830
- stencilZPass: ReplaceStencilOp
3375
+ stencilFunc: NotEqualStencilFunc2,
3376
+ stencilFail: ReplaceStencilOp2,
3377
+ stencilZFail: ReplaceStencilOp2,
3378
+ stencilZPass: ReplaceStencilOp2
2831
3379
  });
2832
3380
  }
2833
3381
  function applyCapTheme(material, theme, dpr) {
@@ -2848,14 +3396,14 @@ function createStencilMaterials(clip) {
2848
3396
  clippingPlanes: clip
2849
3397
  };
2850
3398
  return [
2851
- new MeshBasicMaterial2({
3399
+ new MeshBasicMaterial3({
2852
3400
  ...shared,
2853
3401
  side: BackSide,
2854
3402
  stencilFail: IncrementWrapStencilOp,
2855
3403
  stencilZFail: IncrementWrapStencilOp,
2856
3404
  stencilZPass: IncrementWrapStencilOp
2857
3405
  }),
2858
- new MeshBasicMaterial2({
3406
+ new MeshBasicMaterial3({
2859
3407
  ...shared,
2860
3408
  side: FrontSide,
2861
3409
  stencilFail: DecrementWrapStencilOp,
@@ -2865,15 +3413,15 @@ function createStencilMaterials(clip) {
2865
3413
  ];
2866
3414
  }
2867
3415
  function createMaskMaterial() {
2868
- return new MeshBasicMaterial2({
3416
+ return new MeshBasicMaterial3({
2869
3417
  color: 16777215,
2870
- side: DoubleSide3,
3418
+ side: DoubleSide4,
2871
3419
  stencilWrite: true,
2872
3420
  stencilRef: 0,
2873
- stencilFunc: NotEqualStencilFunc,
2874
- stencilFail: ReplaceStencilOp,
2875
- stencilZFail: ReplaceStencilOp,
2876
- stencilZPass: ReplaceStencilOp
3421
+ stencilFunc: NotEqualStencilFunc2,
3422
+ stencilFail: ReplaceStencilOp2,
3423
+ stencilZFail: ReplaceStencilOp2,
3424
+ stencilZPass: ReplaceStencilOp2
2877
3425
  });
2878
3426
  }
2879
3427
 
@@ -2883,7 +3431,7 @@ var FURNITURE2 = { [EXCLUDE_FROM_FRAME]: true };
2883
3431
  function resolveSectionPlane(options, box) {
2884
3432
  if (!options?.enabled || box.isEmpty()) return null;
2885
3433
  const normal = options.plane?.normal ?? options.normal ?? DEFAULT_SECTION_NORMAL;
2886
- const axis = new Vector310(normal.x, normal.y, normal.z);
3434
+ const axis = new Vector311(normal.x, normal.y, normal.z);
2887
3435
  if (axis.lengthSq() === 0) axis.set(0, 0, 1);
2888
3436
  axis.normalize();
2889
3437
  const bounds = sectionBounds(box, normal);
@@ -2904,7 +3452,7 @@ function resolveSectionPlane(options, box) {
2904
3452
  }
2905
3453
  };
2906
3454
  }
2907
- var Z = new Vector310(0, 0, 1);
3455
+ var Z = new Vector311(0, 0, 1);
2908
3456
  var SectionView = ({
2909
3457
  geometry,
2910
3458
  box,
@@ -2924,7 +3472,7 @@ var SectionView = ({
2924
3472
  const handleRef = useRef4(null);
2925
3473
  const [hovered, setHovered] = useState(false);
2926
3474
  const dragging = useRef4(null);
2927
- const span = useMemo6(() => box.getSize(new Vector310()).length(), [box]);
3475
+ const span = useMemo6(() => box.getSize(new Vector311()).length(), [box]);
2928
3476
  const frameSize = span * SECTION_GIZMO_FRAME_SCALE;
2929
3477
  const directionColor2 = useMemo6(() => sectionDirectionColor(plane.normal), [plane]);
2930
3478
  const resolvedHandleColor = sectionHandleColor(handleColor, directionColor2);
@@ -2955,11 +3503,11 @@ var SectionView = ({
2955
3503
  const maskScene = useMemo6(() => {
2956
3504
  const scene = new Scene();
2957
3505
  const [back, front] = stencils;
2958
- const backMesh = new Mesh3(geometry, back);
3506
+ const backMesh = new Mesh4(geometry, back);
2959
3507
  backMesh.renderOrder = SECTION_RENDER_ORDER.stencil;
2960
- const frontMesh = new Mesh3(geometry, front);
3508
+ const frontMesh = new Mesh4(geometry, front);
2961
3509
  frontMesh.renderOrder = SECTION_RENDER_ORDER.stencil;
2962
- const maskMesh = new Mesh3(quad, createMaskMaterial());
3510
+ const maskMesh = new Mesh4(quad, createMaskMaterial());
2963
3511
  maskMesh.renderOrder = SECTION_RENDER_ORDER.cap;
2964
3512
  scene.add(backMesh, frontMesh, maskMesh);
2965
3513
  return { scene, maskMesh };
@@ -2970,13 +3518,13 @@ var SectionView = ({
2970
3518
  maskScene.maskMesh.quaternion.copy(capQuaternion);
2971
3519
  }, [capPosition, capQuaternion, maskScene]);
2972
3520
  useLayoutEffect2(() => {
2973
- const buffer = gl.getDrawingBufferSize(new Vector25());
3521
+ const buffer = gl.getDrawingBufferSize(new Vector26());
2974
3522
  mask.setSize(buffer.x, buffer.y);
2975
3523
  cap.uniforms.uResolution.value.copy(buffer);
2976
3524
  applyCapTheme(cap, theme, dpr);
2977
3525
  invalidate();
2978
3526
  }, [cap, dpr, gl, invalidate, mask, size, theme]);
2979
- const clearColor = useMemo6(() => new Color4(), []);
3527
+ const clearColor = useMemo6(() => new Color5(), []);
2980
3528
  useFrame4(({ gl: renderer, camera: eye }) => {
2981
3529
  const previousAlpha = renderer.getClearAlpha();
2982
3530
  renderer.getClearColor(clearColor);
@@ -3008,7 +3556,7 @@ var SectionView = ({
3008
3556
  event.stopPropagation();
3009
3557
  const view = camera.position.clone().sub(capPosition).normalize();
3010
3558
  const surface = dragPlane(plane.normal.clone(), view, capPosition);
3011
- const hit = event.ray.intersectPlane(surface, new Vector310());
3559
+ const hit = event.ray.intersectPlane(surface, new Vector311());
3012
3560
  const start = {
3013
3561
  plane: surface,
3014
3562
  from: hit ? hit.dot(plane.normal) : 0,
@@ -3017,7 +3565,7 @@ var SectionView = ({
3017
3565
  dragging.current = start;
3018
3566
  setControlsEnabled(false);
3019
3567
  const raycaster = new Raycaster2();
3020
- const pointer = new Vector25();
3568
+ const pointer = new Vector26();
3021
3569
  const move = (native) => {
3022
3570
  const rect = domElement.getBoundingClientRect();
3023
3571
  pointer.set(
@@ -3025,7 +3573,7 @@ var SectionView = ({
3025
3573
  -((native.clientY - rect.top) / rect.height) * 2 + 1
3026
3574
  );
3027
3575
  raycaster.setFromCamera(pointer, camera);
3028
- const point = raycaster.ray.intersectPlane(start.plane, new Vector310());
3576
+ const point = raycaster.ray.intersectPlane(start.plane, new Vector311());
3029
3577
  if (!point) return;
3030
3578
  onDrag(start.constant - (point.dot(plane.normal) - start.from));
3031
3579
  invalidate();
@@ -3101,7 +3649,7 @@ var SectionView = ({
3101
3649
  color: directionColor2,
3102
3650
  transparent: true,
3103
3651
  opacity: 0.18,
3104
- side: DoubleSide4,
3652
+ side: DoubleSide5,
3105
3653
  depthTest: false,
3106
3654
  depthWrite: false
3107
3655
  }
@@ -3175,10 +3723,11 @@ var PartMesh = ({
3175
3723
  onPick,
3176
3724
  theme,
3177
3725
  showEdges = true,
3726
+ sharpCorners = null,
3178
3727
  display = "solid",
3179
3728
  children
3180
3729
  }) => {
3181
- const { camera, controls, invalidate } = useThree8();
3730
+ const { camera, controls, invalidate, size } = useThree8();
3182
3731
  const context = useMemo7(() => ({ model, geometry }), [model, geometry]);
3183
3732
  const viewerControls = useViewerControls();
3184
3733
  const resolved = useStableTheme(theme);
@@ -3186,6 +3735,7 @@ var PartMesh = ({
3186
3735
  currentTheme.current = resolved;
3187
3736
  const part = useMemo7(() => createPart(model, geometry, currentTheme.current), [geometry, model]);
3188
3737
  const hoverRegion = useRef6(null);
3738
+ const hoverCorner = useRef6(null);
3189
3739
  const onHoverRef = useRef6(onHover);
3190
3740
  onHoverRef.current = onHover;
3191
3741
  const box = useContentBox();
@@ -3216,11 +3766,24 @@ var PartMesh = ({
3216
3766
  const { hoveredFeatureIds: hoveredFeatures, ...rest } = layers.current;
3217
3767
  applyHighlightLayers(
3218
3768
  part,
3219
- { ...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
+ },
3220
3776
  currentTheme.current
3221
3777
  );
3222
3778
  invalidate();
3223
3779
  }, [invalidate, part]);
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]);
3224
3787
  useEffect7(() => () => part.dispose(), [part]);
3225
3788
  const adjacency = useMemo7(() => regionAdjacency(model, geometry), [model, geometry]);
3226
3789
  useEffect7(() => {
@@ -3254,6 +3817,14 @@ var PartMesh = ({
3254
3817
  part.setClippingPlanes(cut ? [cut.plane] : null);
3255
3818
  invalidate();
3256
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]);
3257
3828
  const reportedSection = useRef6("");
3258
3829
  useLayoutEffect3(() => {
3259
3830
  const state = cut?.state ?? null;
@@ -3286,7 +3857,7 @@ var PartMesh = ({
3286
3857
  const target = readTarget(controls);
3287
3858
  const normal = event.face?.normal ?? UP;
3288
3859
  const source = event.nativeEvent;
3289
- return buildPick({
3860
+ const pick = buildPick({
3290
3861
  model,
3291
3862
  region,
3292
3863
  triangleIndex,
@@ -3304,26 +3875,27 @@ var PartMesh = ({
3304
3875
  secondary: "button" in source && source.button === 2
3305
3876
  }
3306
3877
  });
3878
+ const corner = part.sharpCornerAt({ point: event.point, region }, camera, size);
3879
+ return corner ? { ...pick, sharpCorner: corner } : pick;
3307
3880
  };
3308
3881
  const emitHover = (next) => {
3309
3882
  const region = next?.region ?? null;
3310
- if (hoverRegion.current === region) return;
3883
+ const corner = next?.sharpCorner?.index ?? null;
3884
+ if (hoverRegion.current === region && hoverCorner.current === corner) return;
3311
3885
  hoverRegion.current = region;
3886
+ hoverCorner.current = corner;
3887
+ part.setHoveredSharpCorner(corner);
3312
3888
  repaint();
3313
3889
  onHover?.(next);
3314
3890
  };
3315
3891
  useLayoutEffect3(() => {
3316
3892
  if (hover || hoverRegion.current === null) return;
3317
- hoverRegion.current = null;
3318
- repaint();
3319
- onHoverRef.current?.(null);
3320
- }, [hover, repaint]);
3893
+ dropHover();
3894
+ }, [dropHover, hover]);
3321
3895
  useLayoutEffect3(() => {
3322
3896
  if (!engaged || hoverRegion.current === null) return;
3323
- hoverRegion.current = null;
3324
- repaint();
3325
- onHoverRef.current?.(null);
3326
- }, [engaged, repaint]);
3897
+ dropHover();
3898
+ }, [dropHover, engaged]);
3327
3899
  const dragSection = useCallback3(
3328
3900
  (constant) => {
3329
3901
  if (!cut) return;
@@ -3395,9 +3967,9 @@ var PartMesh = ({
3395
3967
  children ? /* @__PURE__ */ jsx6(PartContext.Provider, { value: context, children }) : null
3396
3968
  ] });
3397
3969
  };
3398
- var ORIGIN = new Vector311();
3399
- var UP = new Vector311(0, 0, 1);
3400
- var TARGET = new Vector311();
3970
+ var ORIGIN = new Vector312();
3971
+ var UP = new Vector312(0, 0, 1);
3972
+ var TARGET = new Vector312();
3401
3973
  function readTarget(controls) {
3402
3974
  if (controls && typeof controls.getTarget === "function") {
3403
3975
  return controls.getTarget(TARGET);
@@ -3413,7 +3985,7 @@ function useStableTheme(theme) {
3413
3985
  }
3414
3986
 
3415
3987
  // src/engine/geometry.ts
3416
- import { Matrix4 as Matrix42, Mesh as Mesh4 } from "three";
3988
+ import { Matrix4 as Matrix42, Mesh as Mesh5 } from "three";
3417
3989
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3418
3990
  import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
3419
3991
  var PartMeshError = class extends Error {
@@ -3488,7 +4060,7 @@ function parseStl(data, mesh) {
3488
4060
  function singleMesh(scene) {
3489
4061
  const meshes = [];
3490
4062
  scene.traverse((object) => {
3491
- if (object instanceof Mesh4) meshes.push(object);
4063
+ if (object instanceof Mesh5) meshes.push(object);
3492
4064
  });
3493
4065
  const [mesh] = meshes;
3494
4066
  if (mesh === void 0 || meshes.length !== 1) {
@@ -3524,7 +4096,7 @@ function assertCounts(geometry, expected) {
3524
4096
  }
3525
4097
  function disposeMaterials(scene) {
3526
4098
  scene.traverse((object) => {
3527
- if (!(object instanceof Mesh4)) return;
4099
+ if (!(object instanceof Mesh5)) return;
3528
4100
  for (const material of Array.isArray(object.material) ? object.material : [object.material]) {
3529
4101
  material.dispose();
3530
4102
  }
@@ -3532,7 +4104,7 @@ function disposeMaterials(scene) {
3532
4104
  }
3533
4105
 
3534
4106
  // src/engine/normals.ts
3535
- import { Float32BufferAttribute as Float32BufferAttribute4 } from "three";
4107
+ import { Float32BufferAttribute as Float32BufferAttribute5 } from "three";
3536
4108
  function smoothRegionNormals(geometry, regions) {
3537
4109
  const position = geometry.getAttribute("position");
3538
4110
  if (!position || geometry.index) return;
@@ -3598,7 +4170,7 @@ function smoothRegionNormals(geometry, regions) {
3598
4170
  normals[vertex * 3 + 1] = y / length;
3599
4171
  normals[vertex * 3 + 2] = z / length;
3600
4172
  }
3601
- geometry.setAttribute("normal", new Float32BufferAttribute4(normals, 3));
4173
+ geometry.setAttribute("normal", new Float32BufferAttribute5(normals, 3));
3602
4174
  }
3603
4175
 
3604
4176
  // src/engine/geometry-cache.ts
@@ -4138,11 +4710,14 @@ export {
4138
4710
  useTapGuard,
4139
4711
  visualSurfaces,
4140
4712
  regionEdgesGeometry,
4713
+ regionAdjacency,
4714
+ surfaceCreases,
4715
+ findSharpCorners,
4716
+ sharpCornerPositions,
4141
4717
  REGION_ATTRIBUTE,
4142
4718
  buildRegionTexels,
4143
4719
  buildRegionAttribute,
4144
4720
  createPart,
4145
- regionAdjacency,
4146
4721
  FEATURE_TYPE_RANKS,
4147
4722
  featureTypeRank,
4148
4723
  rankOwners,