@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
@@ -0,0 +1,477 @@
1
+ import {
2
+ BlueprintSketcher,
3
+ getOC,
4
+ type Blueprint,
5
+ type Face,
6
+ type Wire,
7
+ type Point2D,
8
+ } from 'replicad';
9
+ import {castOwnedShape} from './kernel-shapes.js';
10
+ import type * as hb from 'harfbuzzjs';
11
+ import {boolean as combinePaths} from 'flo-boolean';
12
+ import {fontParts, shapeFontText, type Font, type FontPart} from './font.js';
13
+ import {cachedArtifact} from './cached.js';
14
+ import {kernelOperationKey, type KernelArtifact} from './kernel-cache.js';
15
+
16
+ export type TextOptions = Readonly<{
17
+ /** Extra model-unit spacing between glyphs, including spaces. Defaults to 0; may be negative. */
18
+ letterSpacing?: number;
19
+ /** Apply the font's kerning pairs. Defaults to true. */
20
+ kerning?: boolean;
21
+ }>;
22
+
23
+ export type PathCommand =
24
+ | {type: 'M'; x: number; y: number}
25
+ | {type: 'L'; x: number; y: number}
26
+ | {type: 'Q'; x1: number; y1: number; x: number; y: number}
27
+ | {
28
+ type: 'C';
29
+ x1: number;
30
+ y1: number;
31
+ x2: number;
32
+ y2: number;
33
+ x: number;
34
+ y: number;
35
+ }
36
+ | {type: 'Z'};
37
+ type Contour = readonly PathCommand[];
38
+ type Region = readonly Contour[];
39
+ type Glyph = Readonly<{
40
+ regions: KernelArtifact<readonly Region[]>;
41
+ x: number;
42
+ y: number;
43
+ }>;
44
+
45
+ /** Layout shares a baseline at (0, 0, 0); spaces advance without creating faces. */
46
+ export function textGlyphs(
47
+ content: string,
48
+ font: Font,
49
+ size: number,
50
+ options: TextOptions = {},
51
+ ): readonly Glyph[] {
52
+ if (!Number.isFinite(size) || size <= 0)
53
+ throw new Error('Text size must be finite and greater than zero.');
54
+ if (typeof content !== 'string')
55
+ throw new Error('Text content must be a string.');
56
+ const letterSpacing = options.letterSpacing ?? 0;
57
+ if (!Number.isFinite(letterSpacing))
58
+ throw new Error('Text letter spacing must be finite.');
59
+ if (/[\r\n\t]/u.test(content))
60
+ throw new Error(
61
+ 'text() supports one line; position separate text() calls for multiple lines.',
62
+ );
63
+ const parts = fontParts(font);
64
+ const runs: {part: FontPart; content: string}[] = [];
65
+ for (const character of content) {
66
+ const code = character.codePointAt(0)!;
67
+ const part = parts.find(
68
+ ({artifact, ranges}) =>
69
+ (!ranges.length ||
70
+ ranges.some(([start, end]) => code >= start && code <= end)) &&
71
+ artifact.value.font.nominalGlyph(code),
72
+ );
73
+ if (!part) {
74
+ throw new Error(
75
+ `Font ${font.family} has no glyph for ${JSON.stringify(character)} (U+${code.toString(16).toUpperCase()}).`,
76
+ );
77
+ }
78
+ const previous = runs.at(-1);
79
+ if (previous?.part === part) previous.content += character;
80
+ else runs.push({part, content: character});
81
+ }
82
+ const glyphs: Glyph[] = [];
83
+ let advanceX = 0;
84
+ let advanceY = 0;
85
+ for (const {part, content: run} of runs) {
86
+ const resource = part.artifact;
87
+ const parsed = resource.value;
88
+ const scale = size / parsed.face.upem;
89
+ const {infos, positions} = shapeFontText(
90
+ parsed.font,
91
+ run,
92
+ options.kerning ?? true,
93
+ );
94
+ infos.forEach((info, index) => {
95
+ const position = positions[index];
96
+ const regions = textGlyph(resource, info.codepoint, size);
97
+ glyphs.push({
98
+ regions,
99
+ x: advanceX + position.xOffset * scale + glyphs.length * letterSpacing,
100
+ y: -advanceY - position.yOffset * scale,
101
+ });
102
+ advanceX += position.xAdvance * scale;
103
+ advanceY += position.yAdvance * scale;
104
+ });
105
+ }
106
+ return glyphs;
107
+ }
108
+
109
+ const textGlyph = cachedArtifact(
110
+ (
111
+ resource: FontPart['artifact'],
112
+ codepoint: number,
113
+ size: number,
114
+ ): readonly Region[] =>
115
+ groupTextContours(
116
+ splitContours(
117
+ resource.value.font
118
+ .glyphToJson(codepoint)
119
+ .map(command =>
120
+ scaledCommand(command, size / resource.value.face.upem),
121
+ ),
122
+ ),
123
+ ),
124
+ {
125
+ key: (resource, codepoint, size) =>
126
+ kernelOperationKey('textGlyph', [codepoint, size], [resource]),
127
+ },
128
+ );
129
+
130
+ function scaledCommand(
131
+ {type, values}: hb.SvgPathCommand,
132
+ scale: number,
133
+ ): PathCommand {
134
+ const [x1, y1, x2, y2, x, y] = values.map(
135
+ (value, index) => value * scale * (index % 2 ? -1 : 1),
136
+ );
137
+ switch (type) {
138
+ case 'M':
139
+ case 'L':
140
+ return {type, x: x1, y: y1};
141
+ case 'Q':
142
+ return {type, x1, y1, x: x2, y: y2};
143
+ case 'C':
144
+ return {type, x1, y1, x2, y2, x, y};
145
+ case 'Z':
146
+ return {type};
147
+ default:
148
+ throw new Error(`Unsupported glyph path command: ${type}`);
149
+ }
150
+ }
151
+
152
+ function splitContours(commands: readonly PathCommand[]): readonly Contour[] {
153
+ const contours: PathCommand[][] = [];
154
+ let contour: PathCommand[] | undefined;
155
+ for (const command of commands) {
156
+ if (command.type === 'M') {
157
+ contour = [];
158
+ contours.push(contour);
159
+ }
160
+ if (!contour)
161
+ throw new Error('A font contour must start with a move command.');
162
+ contour.push(command);
163
+ if (command.type === 'Z') contour = undefined;
164
+ }
165
+ if (contour) throw new Error('A font contour must be closed.');
166
+ return contours.filter(contour => contour.length > 2);
167
+ }
168
+
169
+ function blueprint(contour: Contour, x = 0, y = 0): Blueprint {
170
+ const start = contour[0];
171
+ if (start?.type !== 'M')
172
+ throw new Error('A font contour must start with a move command.');
173
+ const sketcher = new BlueprintSketcher([start.x + x, -start.y - y]);
174
+ try {
175
+ let previous = start;
176
+ for (const command of contour.slice(1)) {
177
+ if (command.type === 'Z') return sketcher.close();
178
+ if (command.type === 'M')
179
+ throw new Error('Unexpected move inside a font contour.');
180
+ const end: [number, number] = [command.x + x, -command.y - y];
181
+ if (command.type === 'L') {
182
+ if (command.x !== previous.x || command.y !== previous.y)
183
+ sketcher.lineTo(end);
184
+ } else if (command.type === 'Q') {
185
+ sketcher.quadraticBezierCurveTo(end, [command.x1 + x, -command.y1 - y]);
186
+ } else {
187
+ sketcher.cubicBezierCurveTo(
188
+ end,
189
+ [command.x1 + x, -command.y1 - y],
190
+ [command.x2 + x, -command.y2 - y],
191
+ );
192
+ }
193
+ previous = {type: 'M', x: command.x, y: command.y};
194
+ }
195
+ throw new Error('A font contour must be closed.');
196
+ } catch (error) {
197
+ deleteBlueprint(sketcher.done());
198
+ throw error;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Work around Replicad #278 without relying on contour order. Every boundary
204
+ * belongs to its nearest containing boundary; even depths are filled islands.
205
+ * Overlapping outlines are first combined with the font non-zero fill rule.
206
+ */
207
+ export function groupTextContours(
208
+ contours: readonly Contour[],
209
+ ): readonly Region[] {
210
+ const blueprints: Blueprint[] = [];
211
+ try {
212
+ for (const contour of contours) blueprints.push(blueprint(contour));
213
+ if (
214
+ blueprints.some((a, i) =>
215
+ blueprints.slice(i + 1).some(b => contoursIntersect(a, b)),
216
+ )
217
+ ) {
218
+ blueprints.splice(0).forEach(deleteBlueprint);
219
+ contours = combineContours(contours);
220
+ for (const contour of contours) blueprints.push(blueprint(contour));
221
+ }
222
+ const contains = blueprints.map(() => new Set<number>());
223
+ for (let i = 0; i < blueprints.length; i++) {
224
+ for (let j = i + 1; j < blueprints.length; j++) {
225
+ const a = blueprints[i],
226
+ b = blueprints[j];
227
+ if (containsPoint(a, b.firstPoint)) contains[j].add(i);
228
+ if (containsPoint(b, a.firstPoint)) contains[i].add(j);
229
+ }
230
+ }
231
+ const parents = contains.map(
232
+ containers =>
233
+ [...containers].sort((a, b) => contains[b].size - contains[a].size)[0],
234
+ );
235
+ return contours.flatMap((outer, index) =>
236
+ contains[index].size % 2
237
+ ? []
238
+ : [[outer, ...contours.filter((_, hole) => parents[hole] === index)]],
239
+ );
240
+ } finally {
241
+ blueprints.forEach(deleteBlueprint);
242
+ }
243
+ }
244
+
245
+ /** Keep quadratic/cubic Beziers; no polygon flattening or SVG rounding. */
246
+ function combineContours(contours: readonly Contour[]): readonly Contour[] {
247
+ const paths = contours.map(contour => {
248
+ const curves: number[][][] = [];
249
+ let start: number[] = [];
250
+ let previous: number[] = [];
251
+ for (const command of contour) {
252
+ if (command.type === 'M') {
253
+ start = previous = [command.x, command.y];
254
+ continue;
255
+ }
256
+ if (command.type === 'Z') {
257
+ if (previous[0] !== start[0] || previous[1] !== start[1])
258
+ curves.push([previous, start]);
259
+ continue;
260
+ }
261
+ const end = [command.x, command.y];
262
+ if (command.type === 'L') curves.push([previous, end]);
263
+ else if (command.type === 'Q')
264
+ curves.push([previous, [command.x1, command.y1], end]);
265
+ else
266
+ curves.push([
267
+ previous,
268
+ [command.x1, command.y1],
269
+ [command.x2, command.y2],
270
+ end,
271
+ ]);
272
+ previous = end;
273
+ }
274
+ return curves;
275
+ });
276
+ return combinePaths('OR', paths, {minLoopArea: 0})
277
+ .filter(loop => loop.length)
278
+ .map(loop => {
279
+ const [x, y] = loop[0][0];
280
+ const commands: PathCommand[] = [{type: 'M', x, y}];
281
+ for (const curve of loop) {
282
+ const [x, y] = curve.at(-1)!;
283
+ if (curve.length === 2) commands.push({type: 'L', x, y});
284
+ else if (curve.length === 3)
285
+ commands.push({type: 'Q', x1: curve[1][0], y1: curve[1][1], x, y});
286
+ else
287
+ commands.push({
288
+ type: 'C',
289
+ x1: curve[1][0],
290
+ y1: curve[1][1],
291
+ x2: curve[2][0],
292
+ y2: curve[2][1],
293
+ x,
294
+ y,
295
+ });
296
+ }
297
+ commands.push({type: 'Z'});
298
+ return commands;
299
+ });
300
+ }
301
+
302
+ /** Blueprint.delete() does not release cached per-curve bounds. */
303
+ function deleteBlueprint(value: Blueprint): void {
304
+ for (const curve of value.curves) {
305
+ curve._boundingBox?.delete();
306
+ curve._boundingBox = null;
307
+ }
308
+ value.delete();
309
+ }
310
+
311
+ function contoursIntersect(a: Blueprint, b: Blueprint): boolean {
312
+ if (a.boundingBox.isOut(b.boundingBox)) return false;
313
+ const intersector = new (getOC().Geom2dAPI_InterCurveCurve)();
314
+ try {
315
+ for (const first of a.curves)
316
+ for (const second of b.curves) {
317
+ if (first.boundingBox.isOut(second.boundingBox)) continue;
318
+ intersector.Init(first.wrapped, second.wrapped, 1e-9);
319
+ if (intersector.NbPoints() || intersector.NbSegments()) return true;
320
+ }
321
+ return false;
322
+ } finally {
323
+ intersector.delete();
324
+ }
325
+ }
326
+
327
+ function containsPoint(contour: Blueprint, point: Point2D): boolean {
328
+ const [min, max] = contour.boundingBox.bounds;
329
+ if (
330
+ point[0] < min[0] ||
331
+ point[0] > max[0] ||
332
+ point[1] < min[1] ||
333
+ point[1] > max[1]
334
+ )
335
+ return false;
336
+ const span = Math.max(max[0] - min[0], max[1] - min[1]);
337
+ const intersector = new (getOC().Geom2dAPI_InterCurveCurve)();
338
+ try {
339
+ // A ray through a vertex or tangent cannot be counted by intersection parity.
340
+ // Retry a different direction in those cases, retaining exact native curves.
341
+ for (let attempt = 0; attempt < 16; attempt++) {
342
+ const end: Point2D = [
343
+ max[0] + span,
344
+ point[1] + span * Math.sin(attempt * 2.399963229728653),
345
+ ];
346
+ const ray = new BlueprintSketcher(point).lineTo(end).done();
347
+ try {
348
+ let crossings = 0,
349
+ ambiguous = false;
350
+ for (const curve of contour.curves) {
351
+ intersector.Init(ray.curves[0].wrapped, curve.wrapped, 1e-9);
352
+ if (intersector.NbSegments()) {
353
+ ambiguous = true;
354
+ break;
355
+ }
356
+ const endpoints = [curve.firstPoint, curve.lastPoint];
357
+ for (let index = 1; index <= intersector.NbPoints(); index++) {
358
+ const hit = intersector.Point(index);
359
+ try {
360
+ if (
361
+ endpoints.some(
362
+ p => Math.hypot(p[0] - hit.X(), p[1] - hit.Y()) < 1e-8,
363
+ )
364
+ )
365
+ ambiguous = true;
366
+ } finally {
367
+ hit.delete();
368
+ }
369
+ }
370
+ if (ambiguous) break;
371
+ crossings += intersector.NbPoints();
372
+ }
373
+ if (!ambiguous) return crossings % 2 === 1;
374
+ } finally {
375
+ deleteBlueprint(ray);
376
+ }
377
+ }
378
+ throw new Error('Could not determine containment of the font contours.');
379
+ } finally {
380
+ intersector.delete();
381
+ }
382
+ }
383
+
384
+ /** Own every native handle; Replicad's sketchOnPlane leaves edge builders alive. */
385
+ export function textRegionFace(region: Region, x: number, y: number): Face {
386
+ const oc = getOC();
387
+ const blueprints: Blueprint[] = [];
388
+ const wires: Wire[] = [];
389
+ const origin = new oc.gp_Pnt(0, 0, 0);
390
+ const normal = new oc.gp_Dir(0, 1, 0);
391
+ const xDirection = new oc.gp_Dir(1, 0, 0);
392
+ const axis = new oc.gp_Ax2(origin, normal, xDirection);
393
+ const plane = new oc.gp_Pln(origin, normal);
394
+ try {
395
+ for (const contour of region) {
396
+ const profile = blueprint(contour, x, y);
397
+ blueprints.push(profile);
398
+ const wireBuilder = new oc.BRepBuilderAPI_MakeWire();
399
+ try {
400
+ for (const curve of profile.curves) {
401
+ const nativeCurve = oc.GeomLib.To3d(axis, curve.wrapped);
402
+ try {
403
+ const edgeBuilder = new oc.BRepBuilderAPI_MakeEdge(nativeCurve);
404
+ try {
405
+ const edge = edgeBuilder.Edge();
406
+ try {
407
+ wireBuilder.Add(edge);
408
+ } finally {
409
+ edge.delete();
410
+ }
411
+ } finally {
412
+ edgeBuilder.delete();
413
+ }
414
+ } finally {
415
+ nativeCurve.delete();
416
+ }
417
+ }
418
+ if (!wireBuilder.IsDone())
419
+ throw new Error('Could not connect the font contour into a wire.');
420
+ wires.push(castOwnedShape(wireBuilder.Wire()) as Wire);
421
+ } finally {
422
+ wireBuilder.delete();
423
+ }
424
+ }
425
+ const faceBuilder = new oc.BRepBuilderAPI_MakeFace(
426
+ plane,
427
+ wires[0].wrapped,
428
+ true,
429
+ );
430
+ try {
431
+ for (const hole of wires.slice(1)) faceBuilder.Add(hole.wrapped);
432
+ if (!faceBuilder.IsDone())
433
+ throw new Error('Could not create a text face.');
434
+ const raw = faceBuilder.Face();
435
+ try {
436
+ const fixer = new oc.ShapeFix_Face(raw);
437
+ try {
438
+ fixer.FixOrientation();
439
+ const face = castOwnedShape(fixer.Face()) as Face;
440
+ try {
441
+ const analyzer = new oc.BRepCheck_Analyzer(
442
+ face.wrapped,
443
+ true,
444
+ false,
445
+ );
446
+ try {
447
+ if (!analyzer.IsValid())
448
+ throw new Error(
449
+ 'The font contours do not form a valid planar face.',
450
+ );
451
+ } finally {
452
+ analyzer.delete();
453
+ }
454
+ return face;
455
+ } catch (error) {
456
+ face.delete();
457
+ throw error;
458
+ }
459
+ } finally {
460
+ fixer.delete();
461
+ }
462
+ } finally {
463
+ raw.delete();
464
+ }
465
+ } finally {
466
+ faceBuilder.delete();
467
+ }
468
+ } finally {
469
+ wires.forEach(wire => wire.delete());
470
+ blueprints.forEach(deleteBlueprint);
471
+ plane.delete();
472
+ axis.delete();
473
+ xDirection.delete();
474
+ normal.delete();
475
+ origin.delete();
476
+ }
477
+ }
@@ -107,6 +107,8 @@ function topologyShapes(shape: AnyShape, kind: TopologyKind): AnyShape[] {
107
107
  type StableTopology = Readonly<{
108
108
  /** IDs aligned with the corresponding shape traversal order. */
109
109
  ids: readonly TopologyId[];
110
+ /** Next local ID; retained across edits so retired IDs cannot be reused. */
111
+ nextId: number;
110
112
  }>;
111
113
 
112
114
  export type VertexTopology = StableTopology;
@@ -172,6 +174,7 @@ type StableEdgeGroup = Readonly<{
172
174
  start: number;
173
175
  count: number;
174
176
  edgeId: EdgeId;
177
+ linear: boolean;
175
178
  }>;
176
179
 
177
180
  type StableSurfaceGroup = Readonly<{
@@ -254,7 +257,31 @@ export function booleanWithTopology(
254
257
  let result: Shape3D | undefined;
255
258
  try {
256
259
  builder.Build();
260
+ if (!builder.IsDone())
261
+ throw new Error(
262
+ `Could not complete the ${operation === 'fuse' ? 'union' : operation} operation.`,
263
+ );
257
264
  builder.SimplifyResult(true, true, 0.001);
265
+ if (operation === 'intersect') {
266
+ const shape = builder.Shape();
267
+ try {
268
+ const solids = new oc.TopExp_Explorer(
269
+ shape,
270
+ oc.TopAbs_ShapeEnum.TopAbs_SOLID,
271
+ oc.TopAbs_ShapeEnum.TopAbs_SHAPE,
272
+ );
273
+ try {
274
+ if (!solids.More())
275
+ throw new Error(
276
+ 'The inputs have no common solid volume. Adjust their positions or dimensions so they overlap.',
277
+ );
278
+ } finally {
279
+ solids.delete();
280
+ }
281
+ } finally {
282
+ shape.delete();
283
+ }
284
+ }
258
285
  result = castOwnedShape3D(builder.Shape());
259
286
  return {
260
287
  shape: result,
@@ -281,7 +308,17 @@ export function stableEdgeGroups(
281
308
  topology,
282
309
  groups.map(group => group.edgeId),
283
310
  );
284
- return groups.map((group, index) => ({...group, edgeId: ids[index]}));
311
+ const linear = new Map(
312
+ edges.map(edge => [
313
+ edge.hashCode,
314
+ (edge as ReplicadEdge).geomType === 'LINE',
315
+ ]),
316
+ );
317
+ return groups.map((group, index) => ({
318
+ ...group,
319
+ edgeId: ids[index],
320
+ linear: linear.get(group.edgeId) ?? false,
321
+ }));
285
322
  } finally {
286
323
  deleteShapes(edges);
287
324
  }
@@ -484,7 +521,7 @@ function modifyEdges(
484
521
  return {
485
522
  shape: result,
486
523
  topology: transferShapeTopology(
487
- [{shape, topology: source, index: 1}],
524
+ [{shape, topology: source, namespace: 'preserve'}],
488
525
  result,
489
526
  builder,
490
527
  ),
@@ -570,7 +607,7 @@ function resolveTopologyIndex(
570
607
  return index;
571
608
  }
572
609
 
573
- function assertTopologyId(kind: TopologyKind, id: TopologyId): void {
610
+ export function assertTopologyId(kind: TopologyKind, id: TopologyId): void {
574
611
  if (!isTopologyId(id)) {
575
612
  throw new Error(
576
613
  `${kind[0].toUpperCase()}${kind.slice(1)} IDs must be positive integers or paths of at least two positive integers; received ${JSON.stringify(id)}.`,
@@ -581,8 +618,8 @@ function assertTopologyId(kind: TopologyKind, id: TopologyId): void {
581
618
  export type TopologyInput = Readonly<{
582
619
  shape: AnyShape;
583
620
  topology: ShapeTopology;
584
- /** Omitted only for intermediate results inside a single operation. */
585
- index?: number;
621
+ /** Input prefix, unchanged namespace for edits, or an internal operation step. */
622
+ namespace: number | 'preserve' | 'intermediate';
586
623
  }>;
587
624
 
588
625
  /** Returns owned handles, released by the transfer after matching. */
@@ -601,23 +638,26 @@ export function transferShapeTopology(
601
638
  const transfer = (kind: TopologyKind): StableTopology => {
602
639
  const originals: AnyShape[][] = [];
603
640
  const results = topologyShapes(output, kind);
641
+ let nextId = 1;
604
642
  try {
605
643
  const candidates = inputs.flatMap((input, inputIndex) => {
606
644
  const shapes = topologyShapes(input.shape, kind);
607
645
  originals.push(shapes);
608
646
  const source = input.topology[topologyMetadata[kind].plural];
609
647
  assertTopologyLength(kind, shapes, source);
648
+ if (input.namespace === 'preserve')
649
+ nextId = Math.max(nextId, source.nextId);
610
650
  return shapes.map((shape, index) => {
611
651
  const id = source.ids[index];
612
652
  // Numeric elements in an internal prefix are still new to this
613
653
  // operation. Reallocate them with the final traversal, without exposing
614
654
  // internal creation steps or reserving numbers for discarded geometry.
615
655
  const inherited =
616
- input.index !== undefined
617
- ? inheritedTopologyId(input.index, id)
618
- : typeof id === 'number'
619
- ? undefined
620
- : id;
656
+ typeof input.namespace === 'number'
657
+ ? inheritedTopologyId(input.namespace, id)
658
+ : input.namespace === 'preserve' || typeof id !== 'number'
659
+ ? id
660
+ : undefined;
621
661
  const unchanged = matchingOutputShapes(shape.wrapped, results);
622
662
  if (unchanged.length) return {id: inherited, matches: unchanged};
623
663
  if (typeof history !== 'function') {
@@ -658,10 +698,8 @@ export function transferShapeTopology(
658
698
  inherited.set(index, candidate.id);
659
699
  }
660
700
  }
661
- let nextId = 1;
662
- return stableTopology(
663
- results.map((_, index) => inherited.get(index) ?? nextId++),
664
- );
701
+ const ids = results.map((_, index) => inherited.get(index) ?? nextId++);
702
+ return stableTopology(ids, nextId);
665
703
  } finally {
666
704
  originals.forEach(deleteShapes);
667
705
  deleteShapes(results);
@@ -734,11 +772,17 @@ function stableGroupIds(
734
772
  function initialTopology(
735
773
  shapes: readonly (ReplicadVertex | ReplicadEdge | ReplicadFace)[],
736
774
  ): StableTopology {
737
- return stableTopology(shapes.map((_, index) => index + 1));
775
+ return stableTopology(
776
+ shapes.map((_, index) => index + 1),
777
+ shapes.length + 1,
778
+ );
738
779
  }
739
780
 
740
- function stableTopology(ids: readonly TopologyId[]): StableTopology {
741
- return {ids};
781
+ function stableTopology(
782
+ ids: readonly TopologyId[],
783
+ nextId: number,
784
+ ): StableTopology {
785
+ return {ids, nextId};
742
786
  }
743
787
 
744
788
  function shapeTopology(
@@ -6,6 +6,7 @@
6
6
  "rootDir": ".",
7
7
  "outDir": "../../bld/library",
8
8
  "tsBuildInfoFile": "../../.cache/library.tsbuildinfo",
9
- "declarationMap": true
9
+ "declarationMap": true,
10
+ "emitDeclarationOnly": true
10
11
  }
11
12
  }
package/src/node/index.ts CHANGED
@@ -1,12 +1,23 @@
1
- import {fileURLToPath} from 'node:url';
2
1
  import initOpenCascade from '@code3d/opencascade';
3
- import {setOC} from 'replicad';
4
2
  import {init_planegcs_module as initializeSketchSolver} from '@salusoft89/planegcs';
3
+ import * as fontEngine from 'harfbuzzjs';
4
+ import {readFileSync} from 'node:fs';
5
+ import {fileURLToPath} from 'node:url';
6
+ import {
7
+ installFontEngine,
8
+ installModelResourceReader,
9
+ } from '../library/font.js';
10
+ import {installOpenCascade} from '../library/open-cascade.js';
5
11
  import {installSketchSolver} from '../library/sketch-solver.js';
6
12
 
13
+ installModelResourceReader(url =>
14
+ url.protocol === 'file:' ? readFileSync(url) : undefined,
15
+ );
16
+ installFontEngine(fontEngine);
17
+
7
18
  const wasmPath = fileURLToPath(import.meta.resolve('@code3d/opencascade/wasm'));
8
19
  const openCascade = await initOpenCascade({locateFile: () => wasmPath});
9
- setOC(openCascade);
20
+ installOpenCascade(openCascade);
10
21
  installSketchSolver(
11
22
  await initializeSketchSolver({
12
23
  locateFile: () =>
@@ -6,7 +6,8 @@
6
6
  "rootDir": ".",
7
7
  "outDir": "../../bld/node",
8
8
  "tsBuildInfoFile": "../../.cache/node.tsbuildinfo",
9
- "declarationMap": true
9
+ "declarationMap": true,
10
+ "emitDeclarationOnly": true
10
11
  },
11
12
  "references": [
12
13
  {