partforge 0.70.0 → 0.71.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.
@@ -98,11 +98,25 @@ them loses sub-part caching and mesh-topology gates (`holes`, emptiness), nothin
98
98
  a part (never inside a `beginSubPart`/`endSubPart` bracket), it drops cache partitions that
99
99
  have gone unbuilt for three consecutive rebinds.
100
100
 
101
+ `beginSubPart`/`endSubPart` brackets MAY nest: only the outermost pair opens and
102
+ commits a round, and an inner pair is a balanced no-op. Nesting is real rather than
103
+ theoretical — `buildView` opens a round of its own, so any caller that brackets around
104
+ a view build contains one. A backend that keeps a single open round (rather than a
105
+ stack) must collapse inner pairs this way; committing on the inner `end()` would close
106
+ the outer round early and leave the rest of that build uncached.
107
+
101
108
  Sub-part brackets bound cache RETENTION, not reuse: a solid one sub-part builds is reused
102
109
  by any other that asks for the same content hash, so a sheet of identical cells split
103
110
  across row sub-parts evaluates each distinct cell once rather than once per row. An adopted
104
111
  entry is retained by both partitions and disposed only when the last one drops it.
105
112
 
113
+ The oracle (`buildView`, `assemblyOverlaps`) brackets under partition names of its
114
+ own rather than the display sub-part names, and a host adding another oracle-side build
115
+ should do the same. Both reuse the display build's solids through the cross-partition
116
+ index, so measuring a view costs almost nothing right after drawing it; keeping them in
117
+ separate partitions is what stops a measurement's own geometry — verify walks cases with
118
+ params of their own — from displacing the geometry the viewer is showing.
119
+
106
120
  **Transform hoisting.** Booleans commute with rigid transforms, so a conforming backend MAY
107
121
  lift a transform every operand shares out of the boolean and apply it to the result
108
122
  instead — which is what lets N identically-built copies share one evaluation. Two
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "partforge",
3
- "version": "0.70.0",
3
+ "version": "0.71.0",
4
4
  "description": "Turn a declarative part definition into a parametric-CAD web app (three.js + Manifold/Replicad). Requires a Vite-based consumer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -9,10 +9,17 @@ import { viewSubParts, resolveParams, buildPosed } from "./part-model.js";
9
9
  // → [{ a, b, volume, location }] for each offending pair (empty = no collisions)
10
10
  export function assemblyOverlaps(kernel, part, view, params = {}, { tolerance = 1 } = {}) {
11
11
  const { p, d } = resolveParams(part, params);
12
- const posed = viewSubParts(part, view, p).map((name) => ({
13
- name,
14
- solid: buildPosed(kernel, part, name, { purpose: "display", view, p, d }),
15
- }));
12
+ // Same posed solids buildView builds, so this round is almost entirely hits off
13
+ // that one; only the pairwise intersects below are new. Its own oracle partition,
14
+ // for the reason buildView's comment gives.
15
+ kernel.beginSubPart?.(`oracle:overlaps:${view}`);
16
+ let posed;
17
+ try {
18
+ posed = viewSubParts(part, view, p).map((name) => ({
19
+ name,
20
+ solid: buildPosed(kernel, part, name, { purpose: "display", view, p, d }),
21
+ }));
22
+ } catch (e) { kernel.endSubPart?.(); throw e; } // never strand the round on a failed build
16
23
 
17
24
  const overlaps = [];
18
25
  for (let i = 0; i < posed.length; i++) {
@@ -27,6 +34,7 @@ export function assemblyOverlaps(kernel, part, view, params = {}, { tolerance =
27
34
  }
28
35
  }
29
36
  }
30
- kernel.cleanup?.(); // free the per-check WASM objects
37
+ kernel.endSubPart?.();
38
+ kernel.cleanup?.(); // free the per-check WASM objects (cached solids are pinned)
31
39
  return overlaps;
32
40
  }
@@ -15,6 +15,7 @@ export function createSolidCache() {
15
15
  const lastBuilt = new Map(); // name -> rebind generation of the partition's last begin()
16
16
  let generation = 0; // bumped only by sweep() (i.e. per part rebind)
17
17
  let name = null, active = null, prev = null;
18
+ let depth = 0; // bracket nesting; only the OUTERMOST one is real (see begin)
18
19
  let hits = 0, misses = 0;
19
20
 
20
21
  // One partition stops retaining `entry`. Disposal waits for the LAST holder:
@@ -30,9 +31,19 @@ export function createSolidCache() {
30
31
  };
31
32
 
32
33
  return {
33
- begin(n) { name = n; lastBuilt.set(n, generation); prev = caches.get(n) ?? new Map(); active = new Map(); },
34
+ // Nested brackets collapse into the outermost one. There is a single open
35
+ // round (name/active/prev), not a stack, so an inner begin() would otherwise
36
+ // rebind it and the inner end() would commit-and-close the OUTER round early —
37
+ // evicting its entries and leaving the rest of that build uncached. Callers
38
+ // nest legitimately now that buildView brackets: an outer bracket is free to
39
+ // contain one, and the inner pair becomes a no-op.
40
+ begin(n) {
41
+ if (depth++ > 0) return;
42
+ name = n; lastBuilt.set(n, generation); prev = caches.get(n) ?? new Map(); active = new Map();
43
+ },
34
44
 
35
45
  end() {
46
+ if (depth > 0 && --depth > 0) return; // inner bracket — the outer one still owns the round
36
47
  if (name == null) return;
37
48
  for (const [hash, entry] of prev) {
38
49
  if (!active.has(hash)) release(hash, entry); // this partition drops it
@@ -7,8 +7,19 @@ import { viewSubParts, resolveParams, buildPosed } from "../part-model.js";
7
7
  // before they free the kernel. Meshes are JS-owned arrays and survive cleanup.
8
8
  export function buildView(kernel, part, view, params = {}) {
9
9
  const { p, d } = resolveParams(part, params);
10
- return viewSubParts(part, view, p).map((name) => {
11
- const solid = buildPosed(kernel, part, name, { purpose: "display", view, p, d });
12
- return { name, solid, mesh: solid.toMesh() };
13
- });
10
+ // Cache round for the whole view. The name is the ORACLE's, deliberately not the
11
+ // display sub-part names the generate path brackets under: a distinct partition
12
+ // still reuses those solids (the cache indexes entries by content hash across
13
+ // partitions), while keeping this round's own eviction away from the geometry the
14
+ // viewer is showing — bracketing under the display names would make running the
15
+ // oracle throw away the display cache. One name per view also bounds retention:
16
+ // verify walks its cases through here, so each case evicts the previous rather
17
+ // than accumulating every case's geometry at once.
18
+ kernel.beginSubPart?.(`oracle:view:${view}`);
19
+ try {
20
+ return viewSubParts(part, view, p).map((name) => {
21
+ const solid = buildPosed(kernel, part, name, { purpose: "display", view, p, d });
22
+ return { name, solid, mesh: solid.toMesh() };
23
+ });
24
+ } finally { kernel.endSubPart?.(); }
14
25
  }