partforge 0.3.3 → 0.5.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.
@@ -0,0 +1,54 @@
1
+ // Pure core: turn a backend-agnostic raycast hit into a semantic Selection.
2
+ // No three.js, no DOM, no kernel — only the param-deps read-key analysis.
3
+ import { subPartReadKeys, RELEVANT_ALL } from "../param-deps.js";
4
+
5
+ const COS_3DEG = 0.99863; // a normal within 3° of an axis snaps to that axis
6
+ const q2 = (x) => { const r = Math.round(x * 100) / 100; return r === 0 ? 0 : r; }; // 0.01mm, kill -0
7
+
8
+ export function quantizePoint(p) {
9
+ return [q2(p[0]), q2(p[1]), q2(p[2])];
10
+ }
11
+
12
+ export function snapNormal(n) {
13
+ const len = Math.hypot(n[0], n[1], n[2]) || 1;
14
+ const u = [n[0] / len, n[1] / len, n[2] / len];
15
+ let ai = 0; // index of the dominant axis
16
+ if (Math.abs(u[1]) > Math.abs(u[ai])) ai = 1;
17
+ if (Math.abs(u[2]) > Math.abs(u[ai])) ai = 2;
18
+ if (Math.abs(u[ai]) >= COS_3DEG) {
19
+ const axis = [0, 0, 0];
20
+ axis[ai] = u[ai] > 0 ? 1 : -1;
21
+ return axis;
22
+ }
23
+ return [q2(u[0]), q2(u[1]), q2(u[2])];
24
+ }
25
+
26
+ // Only the params the clicked sub-part actually reads — "this geometry, at these inputs".
27
+ function scopeParams(part, view, params, subPart) {
28
+ const reads = subPartReadKeys(part, view, params);
29
+ const keys = reads === RELEVANT_ALL
30
+ ? Object.keys(params)
31
+ : [...(reads.get(subPart) ?? Object.keys(params))];
32
+ const out = {};
33
+ for (const k of keys) out[k] = params[k];
34
+ return out;
35
+ }
36
+
37
+ export function resolveSelection(part, ctx, hit) {
38
+ const point = quantizePoint(hit.pointLocal);
39
+ const selection = {
40
+ subPart: hit.subPart,
41
+ point,
42
+ normal: snapNormal(hit.normalLocal),
43
+ params: scopeParams(part, ctx.view, ctx.params, hit.subPart),
44
+ };
45
+ if (hit.face) {
46
+ // L1 — feature.selector is the author's own { dir, inPlane, at, near } vocabulary,
47
+ // so the LLM can drop it straight into a faces(...)/edges(...) call.
48
+ const feature = { kind: hit.face.kind, selector: { near: point } };
49
+ if (hit.face.axis != null) { feature.axis = hit.face.axis; feature.selector.dir = hit.face.axis; }
50
+ if (hit.face.radius != null) feature.radius = hit.face.radius;
51
+ selection.feature = feature;
52
+ }
53
+ return selection;
54
+ }
@@ -76,7 +76,8 @@ export function createViewer(container, part) {
76
76
  const subMesh = Object.fromEntries(
77
77
  names.map((n) => [n, new THREE.Mesh(new THREE.BufferGeometry(), materialFor(n))])
78
78
  );
79
- for (const m of Object.values(subMesh)) {
79
+ for (const [n, m] of Object.entries(subMesh)) {
80
+ m.name = n;
80
81
  m.visible = false;
81
82
  partsGroup.add(m);
82
83
  }
@@ -226,5 +227,17 @@ export function createViewer(container, part) {
226
227
  container.removeChild(renderer.domElement);
227
228
  }
228
229
 
229
- return { showAssembly, hideAssembly, setSubGeometry, resize, dispose, frame, setAutoRotate, setTheme, getCameraState, setCameraState, onCameraEnd, _subCache: subCache };
230
+ // Transient marker at a world-space point visual confirmation of a pick.
231
+ function flashPoint(world) {
232
+ const dot = new THREE.Mesh(
233
+ new THREE.SphereGeometry(1.2, 16, 12),
234
+ new THREE.MeshBasicMaterial({ color: 0xffcc33, depthTest: false })
235
+ );
236
+ dot.renderOrder = 999;
237
+ dot.position.set(world[0], world[1], world[2]);
238
+ scene.add(dot);
239
+ setTimeout(() => { scene.remove(dot); dot.geometry.dispose(); dot.material.dispose(); }, 1200);
240
+ }
241
+
242
+ return { showAssembly, hideAssembly, setSubGeometry, resize, dispose, frame, setAutoRotate, setTheme, getCameraState, setCameraState, onCameraEnd, _subCache: subCache, camera, domElement: renderer.domElement, _subMeshes: subMesh, flashPoint };
230
243
  }