@code3d/core 0.0.1-alpha.0 → 0.0.1-alpha.10

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.
Files changed (154) hide show
  1. package/README.md +43 -807
  2. package/THIRD_PARTY.md +40 -0
  3. package/bld/THIRD_PARTY_NOTICES.txt +182 -0
  4. package/bld/chunks/chunk-5IP37Z5U.js +30 -0
  5. package/bld/chunks/chunk-5IP37Z5U.js.map +7 -0
  6. package/bld/chunks/chunk-7FIFPADJ.js +1 -0
  7. package/bld/chunks/chunk-7FIFPADJ.js.map +7 -0
  8. package/bld/chunks/chunk-HBRLIGQN.js +31 -0
  9. package/bld/chunks/chunk-HBRLIGQN.js.map +7 -0
  10. package/bld/chunks/chunk-IJHQ57VL.js +19 -0
  11. package/bld/chunks/chunk-IJHQ57VL.js.map +7 -0
  12. package/bld/chunks/chunk-TTJOM4DJ.js +26458 -0
  13. package/bld/chunks/chunk-TTJOM4DJ.js.map +7 -0
  14. package/bld/library/bound-solver.d.ts +29 -5
  15. package/bld/library/bound-solver.d.ts.map +1 -1
  16. package/bld/library/cached.d.ts +35 -0
  17. package/bld/library/cached.d.ts.map +1 -0
  18. package/bld/library/font.d.ts +39 -0
  19. package/bld/library/font.d.ts.map +1 -0
  20. package/bld/library/google-font.d.ts +14 -0
  21. package/bld/library/google-font.d.ts.map +1 -0
  22. package/bld/library/index.d.ts +10 -2
  23. package/bld/library/index.d.ts.map +1 -1
  24. package/bld/library/index.js +94 -3
  25. package/bld/library/index.js.map +7 -1
  26. package/bld/library/inspect.d.ts +98 -0
  27. package/bld/library/inspect.d.ts.map +1 -0
  28. package/bld/library/kernel-artifact-codec.d.ts.map +1 -1
  29. package/bld/library/kernel-cache.d.ts +28 -7
  30. package/bld/library/kernel-cache.d.ts.map +1 -1
  31. package/bld/library/loft.d.ts.map +1 -1
  32. package/bld/library/material.d.ts +1 -1
  33. package/bld/library/material.d.ts.map +1 -1
  34. package/bld/library/open-cascade.d.ts +4 -0
  35. package/bld/library/open-cascade.d.ts.map +1 -0
  36. package/bld/library/relation-solver.d.ts +19 -8
  37. package/bld/library/relation-solver.d.ts.map +1 -1
  38. package/bld/library/replicad.d.ts +3 -2
  39. package/bld/library/replicad.d.ts.map +1 -1
  40. package/bld/library/replicad.js +10 -24
  41. package/bld/library/replicad.js.map +7 -1
  42. package/bld/library/retained-memory.d.ts.map +1 -1
  43. package/bld/library/runtime.d.ts +518 -155
  44. package/bld/library/runtime.d.ts.map +1 -1
  45. package/bld/library/sketch-solver.d.ts +1 -1
  46. package/bld/library/sketch-solver.d.ts.map +1 -1
  47. package/bld/library/sketch.d.ts +21 -1
  48. package/bld/library/sketch.d.ts.map +1 -1
  49. package/bld/library/text.d.ts +53 -0
  50. package/bld/library/text.d.ts.map +1 -0
  51. package/bld/library/three.js +3 -3
  52. package/bld/library/three.js.map +7 -1
  53. package/bld/library/topology.d.ts +6 -2
  54. package/bld/library/topology.d.ts.map +1 -1
  55. package/bld/node/index.d.ts.map +1 -1
  56. package/bld/node/index.js +96 -14
  57. package/bld/node/index.js.map +7 -1
  58. package/bld/node/replicad.js +13 -3
  59. package/bld/node/replicad.js.map +7 -1
  60. package/bld/tooling/index.d.ts +18 -15
  61. package/bld/tooling/index.d.ts.map +1 -1
  62. package/bld/tooling/index.js +165 -19
  63. package/bld/tooling/index.js.map +7 -1
  64. package/docs/api.md +764 -0
  65. package/docs/custom-primitives.mdx +58 -0
  66. package/docs/local-coordinates.md +187 -0
  67. package/docs/origins-and-rotation.mdx +105 -0
  68. package/docs/relations.mdx +450 -0
  69. package/docs/runtime.md +297 -0
  70. package/docs/shells.mdx +70 -0
  71. package/docs/sketches.md +83 -0
  72. package/docs/text.md +23 -0
  73. package/docs/topology.md +152 -0
  74. package/docs/values.md +119 -0
  75. package/package.json +20 -7
  76. package/src/library/bound-solver.ts +103 -73
  77. package/src/library/cached.ts +153 -0
  78. package/src/library/font.ts +186 -0
  79. package/src/library/google-font.ts +97 -0
  80. package/src/library/index.ts +53 -5
  81. package/src/library/inspect.ts +176 -0
  82. package/src/library/kernel-artifact-codec.ts +129 -20
  83. package/src/library/kernel-cache.ts +175 -39
  84. package/src/library/loft.ts +9 -1
  85. package/src/library/material.ts +3 -4
  86. package/src/library/open-cascade.ts +13 -0
  87. package/src/library/relation-solver.ts +230 -113
  88. package/src/library/replicad.ts +5 -5
  89. package/src/library/retained-memory.ts +6 -1
  90. package/src/library/runtime.ts +3753 -1181
  91. package/src/library/shell.ts +3 -3
  92. package/src/library/sketch-solver.ts +3 -1
  93. package/src/library/sketch.ts +112 -13
  94. package/src/library/text.ts +477 -0
  95. package/src/library/topology.ts +61 -17
  96. package/src/library/tsconfig.json +2 -1
  97. package/src/node/index.ts +14 -3
  98. package/src/node/tsconfig.json +2 -1
  99. package/src/tooling/index.ts +64 -51
  100. package/src/tooling/tsconfig.json +2 -1
  101. package/bld/library/alignment-geometry.js +0 -322
  102. package/bld/library/alignment-geometry.js.map +0 -1
  103. package/bld/library/bound-solver.js +0 -252
  104. package/bld/library/bound-solver.js.map +0 -1
  105. package/bld/library/extrude.js +0 -28
  106. package/bld/library/extrude.js.map +0 -1
  107. package/bld/library/kernel-artifact-codec.js +0 -148
  108. package/bld/library/kernel-artifact-codec.js.map +0 -1
  109. package/bld/library/kernel-cache.js +0 -245
  110. package/bld/library/kernel-cache.js.map +0 -1
  111. package/bld/library/kernel-shapes.js +0 -108
  112. package/bld/library/kernel-shapes.js.map +0 -1
  113. package/bld/library/loft.js +0 -161
  114. package/bld/library/loft.js.map +0 -1
  115. package/bld/library/material.js +0 -176
  116. package/bld/library/material.js.map +0 -1
  117. package/bld/library/model-color.js +0 -36
  118. package/bld/library/model-color.js.map +0 -1
  119. package/bld/library/open-cascade-error.js +0 -22
  120. package/bld/library/open-cascade-error.js.map +0 -1
  121. package/bld/library/relation-solver.js +0 -258
  122. package/bld/library/relation-solver.js.map +0 -1
  123. package/bld/library/retained-memory.js +0 -27
  124. package/bld/library/retained-memory.js.map +0 -1
  125. package/bld/library/runtime.js +0 -2954
  126. package/bld/library/runtime.js.map +0 -1
  127. package/bld/library/shell.js +0 -221
  128. package/bld/library/shell.js.map +0 -1
  129. package/bld/library/sketch-curve-intersections.js +0 -117
  130. package/bld/library/sketch-curve-intersections.js.map +0 -1
  131. package/bld/library/sketch-curves.js +0 -89
  132. package/bld/library/sketch-curves.js.map +0 -1
  133. package/bld/library/sketch-drag-rules.js +0 -522
  134. package/bld/library/sketch-drag-rules.js.map +0 -1
  135. package/bld/library/sketch-face.js +0 -72
  136. package/bld/library/sketch-face.js.map +0 -1
  137. package/bld/library/sketch-incidence.js +0 -114
  138. package/bld/library/sketch-incidence.js.map +0 -1
  139. package/bld/library/sketch-precision.js +0 -55
  140. package/bld/library/sketch-precision.js.map +0 -1
  141. package/bld/library/sketch-regions.js +0 -193
  142. package/bld/library/sketch-regions.js.map +0 -1
  143. package/bld/library/sketch-solver.js +0 -578
  144. package/bld/library/sketch-solver.js.map +0 -1
  145. package/bld/library/sketch.js +0 -650
  146. package/bld/library/sketch.js.map +0 -1
  147. package/bld/library/spatial.js +0 -195
  148. package/bld/library/spatial.js.map +0 -1
  149. package/bld/library/topology-id.js +0 -67
  150. package/bld/library/topology-id.js.map +0 -1
  151. package/bld/library/topology-inspection.js +0 -267
  152. package/bld/library/topology-inspection.js.map +0 -1
  153. package/bld/library/topology.js +0 -455
  154. package/bld/library/topology.js.map +0 -1
@@ -8,12 +8,31 @@ const decoder = new TextDecoder('utf-8', {fatal: true});
8
8
  // references. Native handles never escape their runtime; only BinTools bytes do.
9
9
  type Node = null | boolean | string | number | readonly [string, unknown];
10
10
 
11
+ const binaryTypes: Record<
12
+ string,
13
+ new (buffer: ArrayBuffer, offset?: number, length?: number) => ArrayBufferView
14
+ > = {
15
+ Int8Array,
16
+ Uint8Array,
17
+ Uint8ClampedArray,
18
+ Int16Array,
19
+ Uint16Array,
20
+ Int32Array,
21
+ Uint32Array,
22
+ Float32Array,
23
+ Float64Array,
24
+ BigInt64Array,
25
+ BigUint64Array,
26
+ DataView,
27
+ };
28
+
11
29
  export function encodeKernelArtifact(
12
30
  signature: string,
13
31
  value: unknown,
14
32
  ): Uint8Array {
15
33
  const chunks: Uint8Array[] = [];
16
34
  let offset = 0;
35
+ const references = new Map<object, number>();
17
36
  function binary(kind: string, bytes: Uint8Array): Node {
18
37
  const node: Node = [kind, [offset, bytes.byteLength]];
19
38
  chunks.push(bytes);
@@ -32,21 +51,56 @@ export function encodeKernelArtifact(
32
51
  typeof value === 'boolean'
33
52
  )
34
53
  return value;
54
+ if (typeof value === 'bigint') return ['bigint', String(value)];
55
+ if (typeof value !== 'object')
56
+ throw new Error('Unsupported kernel artifact value');
57
+ const existing = references.get(value);
58
+ if (existing !== undefined) return ['reference', existing];
59
+ const id = references.size;
60
+ references.set(value, id);
61
+ return ['value', [id, encodeObject(value)]];
62
+ }
63
+ function encodeObject(value: object): Node {
35
64
  if (value instanceof Shape)
36
65
  return binary('shape', encodeShape(value as AnyShape));
37
- if (value instanceof Float32Array || value instanceof Uint32Array) {
38
- return binary(
39
- value instanceof Float32Array ? 'float32' : 'uint32',
40
- new Uint8Array(value.buffer, value.byteOffset, value.byteLength),
41
- );
66
+ if (ArrayBuffer.isView(value)) {
67
+ if (!Object.hasOwn(binaryTypes, value.constructor.name))
68
+ throw new Error('Unsupported kernel artifact view');
69
+ return [
70
+ 'view',
71
+ [
72
+ value.constructor.name,
73
+ encode(value.buffer),
74
+ value.byteOffset,
75
+ value instanceof DataView
76
+ ? value.byteLength
77
+ : (value as unknown as {length: number}).length,
78
+ ],
79
+ ];
42
80
  }
43
- if (Array.isArray(value)) return ['array', value.map(encode)];
81
+ if (value instanceof ArrayBuffer)
82
+ return binary('ArrayBuffer', new Uint8Array(value));
83
+ if (value instanceof Date) return ['date', encode(value.getTime())];
84
+ if (value instanceof Map)
85
+ return [
86
+ 'map',
87
+ [...value].map(([key, item]) => [encode(key), encode(item)]),
88
+ ];
89
+ if (value instanceof Set) return ['set', [...value].map(encode)];
90
+ if (Array.isArray(value))
91
+ return [
92
+ 'array',
93
+ [
94
+ value.length,
95
+ Object.entries(value).map(([key, item]) => [key, encode(item)]),
96
+ ],
97
+ ];
44
98
  if (
45
- typeof value === 'object' &&
46
- Object.getPrototypeOf(value) === Object.prototype
99
+ Object.getPrototypeOf(value) === Object.prototype ||
100
+ Object.getPrototypeOf(value) === null
47
101
  ) {
48
102
  return [
49
- 'object',
103
+ Object.getPrototypeOf(value) === null ? 'record' : 'object',
50
104
  Object.entries(value).map(([key, item]) => [key, encode(item)]),
51
105
  ];
52
106
  }
@@ -81,23 +135,79 @@ export function decodeKernelArtifact<Value>(
81
135
  if (storedSignature !== signature)
82
136
  throw new Error('Kernel artifact signature mismatch');
83
137
  const shapes: AnyShape[] = [];
84
- function decode(node: Node): unknown {
138
+ const references = new Map<number, unknown>();
139
+ function decode(node: Node, referenceId?: number): unknown {
140
+ const register = <T>(value: T): T => {
141
+ if (referenceId !== undefined) references.set(referenceId, value);
142
+ return value;
143
+ };
85
144
  if (!Array.isArray(node)) return node;
86
145
  const [kind, data] = node;
87
146
  switch (kind) {
147
+ case 'reference':
148
+ if (!references.has(data as number))
149
+ throw new Error('Invalid kernel artifact reference');
150
+ return references.get(data as number);
151
+ case 'value': {
152
+ const [id, value] = data as [number, Node];
153
+ return decode(value, id);
154
+ }
88
155
  case 'undefined':
89
156
  return undefined;
90
157
  case 'number':
91
158
  return Number(data);
159
+ case 'bigint':
160
+ return BigInt(data as string);
161
+ case 'date':
162
+ return register(new Date(decode(data as Node) as number));
163
+ case 'map': {
164
+ const map = register(new Map());
165
+ for (const [key, item] of data as [Node, Node][])
166
+ map.set(decode(key), decode(item));
167
+ return map;
168
+ }
169
+ case 'set': {
170
+ const set = register(new Set());
171
+ for (const item of data as Node[]) set.add(decode(item));
172
+ return set;
173
+ }
92
174
  case 'array':
93
- return (data as Node[]).map(decode);
94
- case 'object':
95
- return Object.fromEntries(
96
- (data as [string, Node][]).map(([key, item]) => [key, decode(item)]),
175
+ case 'record':
176
+ case 'object': {
177
+ const object = register(
178
+ kind === 'array'
179
+ ? new Array((data as [number, unknown])[0])
180
+ : kind === 'record'
181
+ ? Object.create(null)
182
+ : {},
97
183
  );
184
+ const entries = (
185
+ kind === 'array' ? (data as [number, unknown])[1] : data
186
+ ) as [string, Node][];
187
+ for (const [key, item] of entries)
188
+ Object.defineProperty(object, key, {
189
+ value: decode(item),
190
+ writable: true,
191
+ configurable: true,
192
+ enumerable: true,
193
+ });
194
+ return object;
195
+ }
196
+ case 'view': {
197
+ const [name, buffer, offset, length] = data as [
198
+ string,
199
+ Node,
200
+ number,
201
+ number,
202
+ ];
203
+ if (!Object.hasOwn(binaryTypes, name))
204
+ throw new Error('Invalid kernel artifact view');
205
+ return register(
206
+ new binaryTypes[name](decode(buffer) as ArrayBuffer, offset, length),
207
+ );
208
+ }
98
209
  case 'shape':
99
- case 'float32':
100
- case 'uint32': {
210
+ case 'ArrayBuffer': {
101
211
  const [offset, length] = data as number[];
102
212
  if (
103
213
  !Number.isSafeInteger(offset) ||
@@ -108,13 +218,12 @@ export function decodeKernelArtifact<Value>(
108
218
  ) {
109
219
  throw new Error('Invalid kernel artifact binary range');
110
220
  }
111
- // Each retained array owns exactly its bytes, not the whole disk record.
221
+ // Each restored backing buffer owns its source bytes, not the disk record.
112
222
  const chunk = bytes.slice(start + offset, start + offset + length);
113
- if (kind === 'float32') return new Float32Array(chunk.buffer);
114
- if (kind === 'uint32') return new Uint32Array(chunk.buffer);
223
+ if (kind === 'ArrayBuffer') return register(chunk.buffer);
115
224
  const shape = decodeShape(chunk);
116
225
  shapes.push(shape);
117
- return shape;
226
+ return register(shape);
118
227
  }
119
228
  default:
120
229
  throw new Error('Invalid kernel artifact node');
@@ -1,17 +1,21 @@
1
- import type {OpenCascadeInstance} from '@code3d/opencascade';
2
- import {getOC} from 'replicad';
3
- import {estimateRetainedBytes} from './retained-memory.js';
4
1
  import {
5
2
  decodeKernelArtifact,
6
3
  encodeKernelArtifact,
7
4
  } from './kernel-artifact-codec.js';
5
+ import {estimateRetainedBytes} from './retained-memory.js';
8
6
 
9
7
  /** The host opens storage before synchronous evaluation and owns its lifetime. */
10
8
  export interface KernelArtifactStore {
9
+ /** Encoded writes retained by the host for the current working version. */
10
+ readonly pendingWriteBytes?: number;
11
11
  get(id: string): Uint8Array | undefined;
12
+ getMany(ids: readonly string[]): readonly (Uint8Array | undefined)[];
12
13
  set(id: string, bytes: Uint8Array): void;
13
14
  touch(id: string): boolean;
15
+ /** Update access order together, returning existence in the same order. */
16
+ touchMany(ids: readonly string[]): readonly boolean[];
14
17
  delete(id: string): void;
18
+ /** Schedule persistence; hosts may complete disk I/O in the background. */
15
19
  flush(): void;
16
20
  }
17
21
 
@@ -23,6 +27,11 @@ export type KernelArtifact<Value> = Readonly<{
23
27
  value: Value;
24
28
  }>;
25
29
 
30
+ export type CacheCodec<Value> = Readonly<{
31
+ encoder(value: Value): Uint8Array;
32
+ decoder(bytes: Uint8Array): Value;
33
+ }>;
34
+
26
35
  export type KernelValueLifecycle<Value> = Readonly<{
27
36
  estimateBytes(value: Value): number;
28
37
  retain(value: Value): Value;
@@ -31,6 +40,7 @@ export type KernelValueLifecycle<Value> = Readonly<{
31
40
  }>;
32
41
 
33
42
  type CacheEntry<Value> = Readonly<{
43
+ persistenceEligible: boolean;
34
44
  estimatedBytes: number;
35
45
  signature: string;
36
46
  value: Value;
@@ -38,11 +48,13 @@ type CacheEntry<Value> = Readonly<{
38
48
  release(value: Value): void;
39
49
  }>;
40
50
 
41
- export function createKernelOperationCache({
51
+ export function createComputationCache({
42
52
  maximumBytes = 2 * 1024 ** 3,
53
+ minimumPersistenceMilliseconds = 1,
43
54
  nativeAllocatedBytes,
44
55
  }: {
45
56
  maximumBytes?: number;
57
+ minimumPersistenceMilliseconds?: number;
46
58
  nativeAllocatedBytes: () => number;
47
59
  }) {
48
60
  const entries = new Map<string, CacheEntry<unknown>>();
@@ -56,10 +68,23 @@ export function createKernelOperationCache({
56
68
  let store: KernelArtifactStore | undefined;
57
69
  let persistentHits = 0;
58
70
  let persistentWrites = 0;
71
+ let persistenceEncodeMilliseconds = 0;
59
72
  let persistenceErrors = 0;
60
73
  const persisted = new Set<string>();
74
+ const pendingPersistence = new Map<string, () => Uint8Array>();
61
75
  let externalBytes = 0;
62
76
 
77
+ /** Adjust historical retention without invalidating the current model. */
78
+ function setKernelCacheBudget(bytes: number): void {
79
+ maximumBytes = bytes;
80
+ evictHistoricalEntries();
81
+ }
82
+
83
+ /** Apply to future computations without changing existing entries' admission. */
84
+ function setKernelCachePersistenceThreshold(milliseconds: number): void {
85
+ minimumPersistenceMilliseconds = milliseconds;
86
+ }
87
+
63
88
  /** The host accounts for in-flight inputs and all auxiliary native heaps. */
64
89
  function setKernelExternalBytes(bytes: number): void {
65
90
  externalBytes = bytes;
@@ -69,6 +94,7 @@ export function createKernelOperationCache({
69
94
  function setKernelArtifactStore(next: KernelArtifactStore | undefined): void {
70
95
  store = next;
71
96
  persisted.clear();
97
+ pendingPersistence.clear();
72
98
  }
73
99
 
74
100
  function accessStore<Result>(
@@ -93,60 +119,101 @@ export function createKernelOperationCache({
93
119
  */
94
120
  function beginKernelOperationEvaluation(
95
121
  checkCancelled?: () => void,
122
+ kind: 'model' | 'inspect' = 'model',
96
123
  ): () => void {
97
124
  const used = new Set<string>();
98
125
  currentEvaluation = {used, checkCancelled};
99
126
  return () => {
100
- for (const id of retainedEvaluation) {
101
- if (!used.has(id)) historicalEntries.set(id, entries.get(id)!);
127
+ flushPendingPersistence();
128
+ if (kind === 'model') {
129
+ for (const id of retainedEvaluation) {
130
+ if (!used.has(id)) historicalEntries.set(id, entries.get(id)!);
131
+ }
132
+ retainedEvaluation = used;
133
+ } else {
134
+ // Inspection owns temporary geometry, without replacing the model's
135
+ // retained working set or accumulating every previously inspected scene.
136
+ for (const id of used) {
137
+ if (!retainedEvaluation.has(id))
138
+ historicalEntries.set(id, entries.get(id)!);
139
+ }
102
140
  }
103
- retainedEvaluation = used;
104
141
  currentEvaluation = undefined;
105
142
  evictHistoricalEntries();
106
143
  accessStore(store => store.flush());
107
144
  };
108
145
  }
109
146
 
110
- /**
111
- * Reuses one complete, deterministic kernel operation. Arguments describe
112
- * scalar semantics; inputs carry the content identities of prior operations.
113
- * The lifecycle keeps the retained value independent from each disposable use.
114
- */
115
- function evaluateKernelOperation<Value>(
116
- operation: string,
117
- arguments_: readonly KernelKeyPart[],
118
- inputs: readonly KernelArtifact<unknown>[],
147
+ function evaluateCachedArtifact<Value>(
148
+ key: KernelOperationKey,
119
149
  lifecycle: KernelValueLifecycle<Value>,
120
150
  compute: () => Value,
151
+ codec?: CacheCodec<Value> | false,
121
152
  ): KernelArtifact<Value> {
122
- const key = kernelOperationKey(operation, arguments_, inputs);
123
- const cached = findKernelOperation(key, lifecycle);
124
- if (cached) return cached;
153
+ const hit = findKernelOperation(key, lifecycle, codec);
154
+ if (hit) return hit;
155
+ const started = performance.now();
125
156
  const value = compute();
126
- acceptKernelOperation(key, lifecycle, value);
157
+ const milliseconds = performance.now() - started;
158
+ acceptKernelOperation(key, lifecycle, value, milliseconds, codec);
127
159
  return {id: key.id, value};
128
160
  }
129
161
 
130
162
  function findKernelOperation<Value>(
131
163
  key: KernelOperationKey,
132
164
  lifecycle: KernelValueLifecycle<Value>,
165
+ codec?: CacheCodec<Value> | false,
166
+ ): KernelArtifact<Value> | undefined {
167
+ return lookup(key, lifecycle, codec, () =>
168
+ accessStore(store => store.get(key.id)),
169
+ );
170
+ }
171
+
172
+ function findKernelOperations<Value>(
173
+ keys: readonly KernelOperationKey[],
174
+ lifecycle: KernelValueLifecycle<Value>,
175
+ codec?: CacheCodec<Value> | false,
176
+ ): readonly (KernelArtifact<Value> | undefined)[] {
177
+ currentEvaluation?.checkCancelled?.();
178
+ const missing =
179
+ codec === false ? [] : keys.filter(key => !entries.has(key.id));
180
+ const bytes = missing.length
181
+ ? accessStore(store => store.getMany(missing.map(key => key.id)))
182
+ : undefined;
183
+ const restored = new Map(
184
+ missing.map((key, index) => [key.id, bytes?.[index]]),
185
+ );
186
+ return keys.map(key =>
187
+ lookup(key, lifecycle, codec, () => restored.get(key.id)),
188
+ );
189
+ }
190
+
191
+ function lookup<Value>(
192
+ key: KernelOperationKey,
193
+ lifecycle: KernelValueLifecycle<Value>,
194
+ codec: CacheCodec<Value> | false | undefined,
195
+ read: () => Uint8Array | undefined,
133
196
  ): KernelArtifact<Value> | undefined {
134
197
  currentEvaluation?.checkCancelled?.();
135
198
  const {id, signature} = key;
136
199
  let cached = entries.get(id) as CacheEntry<Value> | undefined;
137
- if (!cached) {
138
- const bytes = accessStore(store => store.get(id));
200
+ if (!cached && codec !== false) {
201
+ const bytes = read();
139
202
  if (bytes) {
140
203
  let restored: Value;
141
204
  try {
142
- restored = decodeKernelArtifact<Value>(bytes, signature);
205
+ restored = codec
206
+ ? codec.decoder(decodeKernelArtifact<Uint8Array>(bytes, signature))
207
+ : decodeKernelArtifact<Value>(bytes, signature);
143
208
  } catch {
144
209
  persistenceErrors += 1;
145
210
  accessStore(store => store.delete(id));
146
211
  misses += 1;
147
212
  return undefined;
148
213
  }
149
- cached = retainEntry(key, lifecycle, restored);
214
+ // Existing disk records have already been admitted; reading one must
215
+ // not reinterpret decode time as its original computation cost.
216
+ cached = retainEntry(key, lifecycle, restored, true);
150
217
  persistentHits += 1;
151
218
  persisted.add(id);
152
219
  }
@@ -159,7 +226,8 @@ export function createKernelOperationCache({
159
226
  throw new Error(`Kernel operation cache identity collision: ${id}`);
160
227
  hits += 1;
161
228
  const value = cached.instantiate(cached.value);
162
- persist(key, cached.value);
229
+ if (cached.persistenceEligible && codec !== false)
230
+ persist(key, cached.value, codec, true);
163
231
  touchEntry(id, cached as CacheEntry<unknown>);
164
232
  return {id, value};
165
233
  }
@@ -169,6 +237,8 @@ export function createKernelOperationCache({
169
237
  key: KernelOperationKey,
170
238
  lifecycle: KernelValueLifecycle<Value>,
171
239
  value: Value,
240
+ milliseconds: number,
241
+ codec?: CacheCodec<Value> | false,
172
242
  ): void {
173
243
  const existing = entries.get(key.id);
174
244
  if (existing) {
@@ -184,28 +254,74 @@ export function createKernelOperationCache({
184
254
  lifecycle.release(value);
185
255
  throw error;
186
256
  }
187
- const entry = retainEntry(key, lifecycle, retained);
188
- persist(key, retained);
257
+ const entry = retainEntry(
258
+ key,
259
+ lifecycle,
260
+ retained,
261
+ milliseconds >= minimumPersistenceMilliseconds,
262
+ );
263
+ if (entry.persistenceEligible && codec !== false)
264
+ persist(key, retained, codec);
189
265
  touchEntry(key.id, entry as CacheEntry<unknown>);
190
266
  }
191
267
 
192
- function persist(key: KernelOperationKey, value: unknown): void {
268
+ function persist<Value>(
269
+ key: KernelOperationKey,
270
+ value: Value,
271
+ codec?: CacheCodec<Value>,
272
+ defer = false,
273
+ ): void {
193
274
  if (!store || persisted.has(key.id)) return;
275
+ const encode = () => {
276
+ const started = performance.now();
277
+ try {
278
+ return encodeKernelArtifact(
279
+ key.signature,
280
+ codec ? Uint8Array.from(codec.encoder(value)) : value,
281
+ );
282
+ } finally {
283
+ persistenceEncodeMilliseconds += performance.now() - started;
284
+ }
285
+ };
286
+ if (defer && currentEvaluation) {
287
+ pendingPersistence.set(key.id, encode);
288
+ return;
289
+ }
194
290
  accessStore(store => {
195
- if (!store.touch(key.id)) {
196
- store.set(key.id, encodeKernelArtifact(key.signature, value));
291
+ // A completed computation follows a miss; write it without another lookup.
292
+ if (!defer || !store.touch(key.id)) {
293
+ store.set(key.id, encode());
197
294
  persistentWrites += 1;
198
295
  }
199
296
  persisted.add(key.id);
200
297
  });
201
298
  }
202
299
 
300
+ /** Memory hits need one transaction, while newly computed work is saved immediately. */
301
+ function flushPendingPersistence(): void {
302
+ if (!pendingPersistence.size) return;
303
+ accessStore(store => {
304
+ const ids = [...pendingPersistence.keys()];
305
+ const present = store.touchMany(ids);
306
+ ids.forEach((id, index) => {
307
+ if (!present[index]) {
308
+ store.set(id, pendingPersistence.get(id)!());
309
+ persistentWrites += 1;
310
+ }
311
+ persisted.add(id);
312
+ });
313
+ });
314
+ pendingPersistence.clear();
315
+ }
316
+
203
317
  function retainEntry<Value>(
204
318
  key: KernelOperationKey,
205
319
  lifecycle: KernelValueLifecycle<Value>,
206
320
  retained: Value,
321
+ persistenceEligible: boolean,
207
322
  ): CacheEntry<Value> {
208
323
  const entry: CacheEntry<Value> = {
324
+ persistenceEligible,
209
325
  estimatedBytes:
210
326
  256 +
211
327
  estimateRetainedBytes(key.signature) +
@@ -244,8 +360,10 @@ export function createKernelOperationCache({
244
360
  estimatedJavaScriptBytes = 0;
245
361
  persistentHits = 0;
246
362
  persistentWrites = 0;
363
+ persistenceEncodeMilliseconds = 0;
247
364
  persistenceErrors = 0;
248
365
  persisted.clear();
366
+ pendingPersistence.clear();
249
367
  }
250
368
 
251
369
  function kernelOperationCacheStats() {
@@ -258,8 +376,10 @@ export function createKernelOperationCache({
258
376
  nativeAllocatedBytes: nativeAllocatedBytes(),
259
377
  maximumBytes,
260
378
  externalBytes,
379
+ pendingPersistenceBytes: store?.pendingWriteBytes ?? 0,
261
380
  persistentHits,
262
381
  persistentWrites,
382
+ persistenceEncodeMilliseconds,
263
383
  persistenceErrors,
264
384
  };
265
385
  }
@@ -267,7 +387,10 @@ export function createKernelOperationCache({
267
387
  function evictHistoricalEntries(): void {
268
388
  while (
269
389
  historicalEntries.size &&
270
- nativeAllocatedBytes() + estimatedJavaScriptBytes + externalBytes >
390
+ nativeAllocatedBytes() +
391
+ estimatedJavaScriptBytes +
392
+ externalBytes +
393
+ (store?.pendingWriteBytes ?? 0) >
271
394
  maximumBytes
272
395
  ) {
273
396
  const [id, entry] = historicalEntries.entries().next().value!;
@@ -279,29 +402,41 @@ export function createKernelOperationCache({
279
402
  }
280
403
 
281
404
  return {
405
+ evaluateCachedArtifact,
282
406
  beginKernelOperationEvaluation,
283
- evaluateKernelOperation,
284
407
  clearKernelOperationCache,
285
408
  kernelOperationCacheStats,
409
+ setKernelCacheBudget,
410
+ setKernelCachePersistenceThreshold,
286
411
  setKernelArtifactStore,
287
412
  findKernelOperation,
413
+ findKernelOperations,
288
414
  acceptKernelOperation,
289
415
  setKernelExternalBytes,
290
416
  };
291
417
  }
292
418
 
419
+ let nativeMemoryCounter = () => 0;
420
+
421
+ /** The installed backend supplies native accounting; pure caches need no kernel. */
422
+ export function setKernelNativeMemoryCounter(read: () => number): void {
423
+ nativeMemoryCounter = read;
424
+ }
425
+
293
426
  export const {
427
+ evaluateCachedArtifact,
294
428
  beginKernelOperationEvaluation,
295
- evaluateKernelOperation,
296
429
  clearKernelOperationCache,
297
430
  kernelOperationCacheStats,
431
+ setKernelCacheBudget,
432
+ setKernelCachePersistenceThreshold,
298
433
  setKernelArtifactStore,
299
434
  findKernelOperation,
435
+ findKernelOperations,
300
436
  acceptKernelOperation,
301
437
  setKernelExternalBytes,
302
- } = createKernelOperationCache({
303
- nativeAllocatedBytes: () =>
304
- (getOC() as OpenCascadeInstance).Code3dMemory.AllocatedBytes(),
438
+ } = createComputationCache({
439
+ nativeAllocatedBytes: () => nativeMemoryCounter(),
305
440
  });
306
441
 
307
442
  export type KernelOperationKey = Readonly<{id: string; signature: string}>;
@@ -316,16 +451,17 @@ export function kernelOperationKey(
316
451
  arguments_,
317
452
  inputs.map(input => input.id),
318
453
  ]);
319
- return {id: contentId(signature), signature};
454
+ return {id: kernelContentId(signature), signature};
320
455
  }
321
456
 
322
- function contentId(value: string): string {
457
+ export function kernelContentId(value: string | Uint8Array): string {
323
458
  let first = 0x811c9dc5;
324
459
  let second = 0x9e3779b9;
325
460
  let third = 0x85ebca6b;
326
461
  let fourth = 0xc2b2ae35;
327
462
  for (let index = 0; index < value.length; index += 1) {
328
- const code = value.charCodeAt(index);
463
+ const code =
464
+ typeof value === 'string' ? value.charCodeAt(index) : value[index];
329
465
  first = Math.imul(first ^ code, 0x01000193);
330
466
  second = Math.imul(second ^ code, 0x27d4eb2d);
331
467
  third = Math.imul(third ^ code, 0x165667b1);
@@ -62,7 +62,11 @@ export function loftWithTopology(
62
62
  ruled,
63
63
  holes.map(wires => wires[0]),
64
64
  );
65
- return booleanWithTopology(outer, inner, 'cut');
65
+ return booleanWithTopology(
66
+ {...outer, namespace: 'intermediate'},
67
+ {...inner, namespace: 'intermediate'},
68
+ 'cut',
69
+ );
66
70
  } finally {
67
71
  inner?.shape.delete();
68
72
  outer?.shape.delete();
@@ -104,6 +108,10 @@ function loftContoursWithTopology(
104
108
  builder.SetMutableInput(false);
105
109
  for (const wire of wires) builder.AddWire(wire.wrapped);
106
110
  builder.Build();
111
+ if (!builder.IsDone())
112
+ throw new Error(
113
+ 'Could not construct a solid loft through these sections. Adjust their positions, orientations, or profiles.',
114
+ );
107
115
  }
108
116
  result = castOwnedShape3D(builder.Shape());
109
117
  caps[0] = castOwnedShape(builder.FirstShape()) as Face;
@@ -6,7 +6,7 @@ import {
6
6
  type SerializedImage,
7
7
  type Texture,
8
8
  type TypedArray,
9
- } from './three.js';
9
+ } from 'three';
10
10
  import {parseModelColor} from './model-color.js';
11
11
 
12
12
  /** A color selects the default material for each geometry kind. */
@@ -52,14 +52,13 @@ export function captureModelMaterial(
52
52
  }
53
53
  }
54
54
  if ('isShaderMaterial' in input) {
55
- const shader = input as import('./three.js').ShaderMaterial;
55
+ const shader = input as import('three').ShaderMaterial;
56
56
  if (
57
57
  shader.uniformsGroups.length ||
58
58
  shader.index0AttributeName !== undefined ||
59
59
  JSON.stringify(shader.defaultAttributeValues) !==
60
60
  JSON.stringify(
61
- (restored as import('./three.js').ShaderMaterial)
62
- .defaultAttributeValues,
61
+ (restored as import('three').ShaderMaterial).defaultAttributeValues,
63
62
  )
64
63
  ) {
65
64
  throw new Error(
@@ -0,0 +1,13 @@
1
+ import type {OpenCascadeInstance} from '@code3d/opencascade';
2
+ import {setOC} from 'replicad';
3
+ import {
4
+ clearKernelOperationCache,
5
+ setKernelNativeMemoryCounter,
6
+ } from './kernel-cache.js';
7
+
8
+ /** Install the backend and its memory accounting for Node and hosted runtimes. */
9
+ export function installOpenCascade(openCascade: OpenCascadeInstance): void {
10
+ clearKernelOperationCache();
11
+ setOC(openCascade);
12
+ setKernelNativeMemoryCounter(() => openCascade.Code3dMemory.AllocatedBytes());
13
+ }