@code3d/core 0.0.1-alpha.0 → 0.0.1-alpha.2
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.
- package/README.md +223 -786
- package/THIRD_PARTY.md +40 -0
- package/bld/THIRD_PARTY_NOTICES.txt +182 -0
- package/bld/chunks/chunk-5E6VXJU2.js +30 -0
- package/bld/chunks/chunk-5E6VXJU2.js.map +7 -0
- package/bld/chunks/chunk-63ZUQWQ2.js +19 -0
- package/bld/chunks/chunk-63ZUQWQ2.js.map +7 -0
- package/bld/chunks/chunk-AWYI64EE.js +31 -0
- package/bld/chunks/chunk-AWYI64EE.js.map +7 -0
- package/bld/chunks/chunk-F53RTQPT.js +24787 -0
- package/bld/chunks/chunk-F53RTQPT.js.map +7 -0
- package/bld/chunks/chunk-TJIN6S4R.js +1 -0
- package/bld/chunks/chunk-TJIN6S4R.js.map +7 -0
- package/bld/library/cached.d.ts +33 -0
- package/bld/library/cached.d.ts.map +1 -0
- package/bld/library/font.d.ts +39 -0
- package/bld/library/font.d.ts.map +1 -0
- package/bld/library/google-font.d.ts +14 -0
- package/bld/library/google-font.d.ts.map +1 -0
- package/bld/library/index.d.ts +8 -2
- package/bld/library/index.d.ts.map +1 -1
- package/bld/library/index.js +60 -3
- package/bld/library/index.js.map +7 -1
- package/bld/library/kernel-artifact-codec.d.ts.map +1 -1
- package/bld/library/kernel-cache.d.ts +17 -6
- package/bld/library/kernel-cache.d.ts.map +1 -1
- package/bld/library/material.d.ts +1 -1
- package/bld/library/material.d.ts.map +1 -1
- package/bld/library/open-cascade.d.ts +4 -0
- package/bld/library/open-cascade.d.ts.map +1 -0
- package/bld/library/replicad.d.ts +3 -2
- package/bld/library/replicad.d.ts.map +1 -1
- package/bld/library/replicad.js +10 -24
- package/bld/library/replicad.js.map +7 -1
- package/bld/library/retained-memory.d.ts.map +1 -1
- package/bld/library/runtime.d.ts +144 -80
- package/bld/library/runtime.d.ts.map +1 -1
- package/bld/library/sketch-solver.d.ts +1 -1
- package/bld/library/sketch-solver.d.ts.map +1 -1
- package/bld/library/sketch.d.ts +9 -1
- package/bld/library/sketch.d.ts.map +1 -1
- package/bld/library/text.d.ts +53 -0
- package/bld/library/text.d.ts.map +1 -0
- package/bld/library/three.js +3 -3
- package/bld/library/three.js.map +7 -1
- package/bld/node/index.d.ts.map +1 -1
- package/bld/node/index.js +62 -14
- package/bld/node/index.js.map +7 -1
- package/bld/node/replicad.js +13 -3
- package/bld/node/replicad.js.map +7 -1
- package/bld/tooling/index.d.ts +17 -15
- package/bld/tooling/index.d.ts.map +1 -1
- package/bld/tooling/index.js +147 -19
- package/bld/tooling/index.js.map +7 -1
- package/package.json +18 -6
- package/src/library/cached.ts +132 -0
- package/src/library/font.ts +186 -0
- package/src/library/google-font.ts +97 -0
- package/src/library/index.ts +8 -3
- package/src/library/kernel-artifact-codec.ts +129 -20
- package/src/library/kernel-cache.ts +111 -33
- package/src/library/material.ts +3 -4
- package/src/library/open-cascade.ts +13 -0
- package/src/library/replicad.ts +5 -5
- package/src/library/retained-memory.ts +6 -1
- package/src/library/runtime.ts +2049 -1728
- package/src/library/sketch-solver.ts +3 -1
- package/src/library/sketch.ts +81 -13
- package/src/library/text.ts +477 -0
- package/src/library/tsconfig.json +2 -1
- package/src/node/index.ts +14 -3
- package/src/node/tsconfig.json +2 -1
- package/src/tooling/index.ts +48 -46
- package/src/tooling/tsconfig.json +2 -1
- package/bld/library/alignment-geometry.js +0 -322
- package/bld/library/alignment-geometry.js.map +0 -1
- package/bld/library/bound-solver.js +0 -252
- package/bld/library/bound-solver.js.map +0 -1
- package/bld/library/extrude.js +0 -28
- package/bld/library/extrude.js.map +0 -1
- package/bld/library/kernel-artifact-codec.js +0 -148
- package/bld/library/kernel-artifact-codec.js.map +0 -1
- package/bld/library/kernel-cache.js +0 -245
- package/bld/library/kernel-cache.js.map +0 -1
- package/bld/library/kernel-shapes.js +0 -108
- package/bld/library/kernel-shapes.js.map +0 -1
- package/bld/library/loft.js +0 -161
- package/bld/library/loft.js.map +0 -1
- package/bld/library/material.js +0 -176
- package/bld/library/material.js.map +0 -1
- package/bld/library/model-color.js +0 -36
- package/bld/library/model-color.js.map +0 -1
- package/bld/library/open-cascade-error.js +0 -22
- package/bld/library/open-cascade-error.js.map +0 -1
- package/bld/library/relation-solver.js +0 -258
- package/bld/library/relation-solver.js.map +0 -1
- package/bld/library/retained-memory.js +0 -27
- package/bld/library/retained-memory.js.map +0 -1
- package/bld/library/runtime.js +0 -2954
- package/bld/library/runtime.js.map +0 -1
- package/bld/library/shell.js +0 -221
- package/bld/library/shell.js.map +0 -1
- package/bld/library/sketch-curve-intersections.js +0 -117
- package/bld/library/sketch-curve-intersections.js.map +0 -1
- package/bld/library/sketch-curves.js +0 -89
- package/bld/library/sketch-curves.js.map +0 -1
- package/bld/library/sketch-drag-rules.js +0 -522
- package/bld/library/sketch-drag-rules.js.map +0 -1
- package/bld/library/sketch-face.js +0 -72
- package/bld/library/sketch-face.js.map +0 -1
- package/bld/library/sketch-incidence.js +0 -114
- package/bld/library/sketch-incidence.js.map +0 -1
- package/bld/library/sketch-precision.js +0 -55
- package/bld/library/sketch-precision.js.map +0 -1
- package/bld/library/sketch-regions.js +0 -193
- package/bld/library/sketch-regions.js.map +0 -1
- package/bld/library/sketch-solver.js +0 -578
- package/bld/library/sketch-solver.js.map +0 -1
- package/bld/library/sketch.js +0 -650
- package/bld/library/sketch.js.map +0 -1
- package/bld/library/spatial.js +0 -195
- package/bld/library/spatial.js.map +0 -1
- package/bld/library/topology-id.js +0 -67
- package/bld/library/topology-id.js.map +0 -1
- package/bld/library/topology-inspection.js +0 -267
- package/bld/library/topology-inspection.js.map +0 -1
- package/bld/library/topology.js +0 -455
- package/bld/library/topology.js.map +0 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type {GcsSystem
|
|
1
|
+
import type {GcsSystem} from '@salusoft89/planegcs/dist/planegcs_dist/gcs_system.js';
|
|
2
|
+
// Upstream ships this declaration alongside sources, but omits it from dist.
|
|
3
|
+
import type {ModuleStatic} from '@salusoft89/planegcs/planegcs_dist/planegcs.js';
|
|
2
4
|
import type {SketchArcDirection, SketchPosition} from './sketch.js';
|
|
3
5
|
import {sketchArcGeometry} from './sketch-curves.js';
|
|
4
6
|
import {pointLineDistance} from './sketch-incidence.js';
|
package/src/library/sketch.ts
CHANGED
|
@@ -6,9 +6,11 @@ import {
|
|
|
6
6
|
import {solveSketchDrag} from './sketch-drag-rules.js';
|
|
7
7
|
import {sketchRegions} from './sketch-regions.js';
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
SketchFrame,
|
|
10
10
|
disposeModelObjects,
|
|
11
11
|
isModelObject,
|
|
12
|
+
type Constraint,
|
|
13
|
+
type FaceAnchor,
|
|
12
14
|
type FaceModel,
|
|
13
15
|
} from './runtime.js';
|
|
14
16
|
import {
|
|
@@ -85,6 +87,10 @@ export type SketchOptions = Readonly<{
|
|
|
85
87
|
|
|
86
88
|
/** An immutable sketch definition, independent of B-Rep construction. */
|
|
87
89
|
export interface Sketch {
|
|
90
|
+
/** The unbounded local XZ plane (+Y normal), independent of closed regions. */
|
|
91
|
+
readonly plane: FaceAnchor;
|
|
92
|
+
/** Relates this immutable sketch's frame without changing its two-dimensional data. */
|
|
93
|
+
relate(build: (self: Sketch) => Constraint | readonly Constraint[]): Sketch;
|
|
88
94
|
/** References a point defined in this layer. */
|
|
89
95
|
point(id: number): SketchPoint;
|
|
90
96
|
/** Adds a local layer while retaining the upstream sketch as read-only input. */
|
|
@@ -112,11 +118,26 @@ const definitions = new WeakMap<Sketch, Definition>();
|
|
|
112
118
|
const references = new WeakSet<SketchPoint>();
|
|
113
119
|
|
|
114
120
|
class SketchValue implements Sketch {
|
|
121
|
+
readonly frame: SketchFrame;
|
|
122
|
+
readonly source?: Sketch;
|
|
123
|
+
|
|
115
124
|
constructor(
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
input:
|
|
126
|
+
| {
|
|
127
|
+
entries?: readonly SketchEntry[];
|
|
128
|
+
options?: SketchOptions;
|
|
129
|
+
base?: Sketch;
|
|
130
|
+
}
|
|
131
|
+
| {source: Sketch; frame: SketchFrame},
|
|
119
132
|
) {
|
|
133
|
+
if ('source' in input) {
|
|
134
|
+
this.frame = input.frame;
|
|
135
|
+
this.source = input.source;
|
|
136
|
+
definitions.set(this, definitions.get(input.source)!);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const {entries, options, base} = input;
|
|
140
|
+
this.frame = new SketchFrame(base && (base as SketchValue).frame);
|
|
120
141
|
const ids = new Set<number>();
|
|
121
142
|
const points = new Map<number, SketchPosition>();
|
|
122
143
|
const copied = (entries ?? []).map<SketchEntry>(entry => {
|
|
@@ -164,6 +185,10 @@ class SketchValue implements Sketch {
|
|
|
164
185
|
ancestor = definitions.get(ancestor)?.base
|
|
165
186
|
)
|
|
166
187
|
ancestors.add(ancestor);
|
|
188
|
+
const ancestorFor = (value: Sketch) =>
|
|
189
|
+
[...ancestors].find(
|
|
190
|
+
ancestor => definitions.get(ancestor) === definitions.get(value),
|
|
191
|
+
);
|
|
167
192
|
for (const [kind, id, data] of copied) {
|
|
168
193
|
if (kind === 'point' && Array.isArray(data)) continue;
|
|
169
194
|
const refs =
|
|
@@ -180,7 +205,7 @@ class SketchValue implements Sketch {
|
|
|
180
205
|
throw new Error(
|
|
181
206
|
`Sketch ${kind} ${id} references missing local point ${ref}.`,
|
|
182
207
|
);
|
|
183
|
-
} else if (!references.has(ref) || !
|
|
208
|
+
} else if (!references.has(ref) || !ancestorFor(ref.sketch)) {
|
|
184
209
|
throw new Error(
|
|
185
210
|
`Sketch ${kind} ${id} must reference a local or upstream point.`,
|
|
186
211
|
);
|
|
@@ -209,7 +234,7 @@ class SketchValue implements Sketch {
|
|
|
209
234
|
if (
|
|
210
235
|
typeof ref === 'number'
|
|
211
236
|
? !points.has(ref)
|
|
212
|
-
: !references.has(ref) || !
|
|
237
|
+
: !references.has(ref) || !ancestorFor(ref.sketch)
|
|
213
238
|
)
|
|
214
239
|
throw new Error(
|
|
215
240
|
'Sketch constraints must reference a local or upstream point.',
|
|
@@ -304,6 +329,7 @@ class SketchValue implements Sketch {
|
|
|
304
329
|
const local = 'local';
|
|
305
330
|
const layers = new Map<Sketch, string>();
|
|
306
331
|
const identity = (value: Sketch) => {
|
|
332
|
+
value = ancestorFor(value) ?? value;
|
|
307
333
|
let id = layers.get(value);
|
|
308
334
|
if (!id) layers.set(value, (id = `upstream:${layers.size}`));
|
|
309
335
|
return id;
|
|
@@ -345,8 +371,21 @@ class SketchValue implements Sketch {
|
|
|
345
371
|
return ref;
|
|
346
372
|
}
|
|
347
373
|
|
|
374
|
+
get plane(): FaceAnchor {
|
|
375
|
+
return this.frame.plane;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
relate(build: (self: Sketch) => Constraint | readonly Constraint[]): Sketch {
|
|
379
|
+
let related!: SketchValue;
|
|
380
|
+
this.frame.relate(frame => {
|
|
381
|
+
related = new SketchValue({source: this, frame});
|
|
382
|
+
return build(related);
|
|
383
|
+
});
|
|
384
|
+
return related;
|
|
385
|
+
}
|
|
386
|
+
|
|
348
387
|
derive(entries?: readonly SketchEntry[], options?: SketchOptions): Sketch {
|
|
349
|
-
return new SketchValue(entries, options, this);
|
|
388
|
+
return new SketchValue({entries, options, base: this});
|
|
350
389
|
}
|
|
351
390
|
|
|
352
391
|
face(): FaceModel {
|
|
@@ -355,14 +394,14 @@ class SketchValue implements Sketch {
|
|
|
355
394
|
throw new Error(
|
|
356
395
|
`sketch.face() requires exactly one closed region; found ${regions.length}. Use faces() for multiple regions.`,
|
|
357
396
|
);
|
|
358
|
-
return
|
|
397
|
+
return this.frame.face(regions[0]);
|
|
359
398
|
}
|
|
360
399
|
|
|
361
400
|
faces(): readonly FaceModel[] {
|
|
362
401
|
const faces: FaceModel[] = [];
|
|
363
402
|
try {
|
|
364
403
|
for (const region of sketchRegions(this.layers()))
|
|
365
|
-
faces.push(
|
|
404
|
+
faces.push(this.frame.face(region));
|
|
366
405
|
return faces;
|
|
367
406
|
} catch (error) {
|
|
368
407
|
disposeModelObjects(
|
|
@@ -381,7 +420,13 @@ class SketchValue implements Sketch {
|
|
|
381
420
|
)
|
|
382
421
|
values.unshift(value);
|
|
383
422
|
return values.map(value =>
|
|
384
|
-
snapshotSketch(value, s =>
|
|
423
|
+
snapshotSketch(value, s =>
|
|
424
|
+
String(
|
|
425
|
+
values.findIndex(
|
|
426
|
+
value => definitions.get(value) === definitions.get(s),
|
|
427
|
+
),
|
|
428
|
+
),
|
|
429
|
+
),
|
|
385
430
|
);
|
|
386
431
|
}
|
|
387
432
|
}
|
|
@@ -391,7 +436,7 @@ export function sketch(
|
|
|
391
436
|
entries?: readonly SketchEntry[],
|
|
392
437
|
options?: SketchOptions,
|
|
393
438
|
): Sketch {
|
|
394
|
-
return new SketchValue(entries, options);
|
|
439
|
+
return new SketchValue({entries, options});
|
|
395
440
|
}
|
|
396
441
|
|
|
397
442
|
export function isSketch(value: unknown): value is Sketch {
|
|
@@ -402,6 +447,16 @@ export function sketchDefinition(value: Sketch): Definition {
|
|
|
402
447
|
return definitions.get(value)!;
|
|
403
448
|
}
|
|
404
449
|
|
|
450
|
+
/** The preceding spatial value; it shares a geometry definition, not a derived layer. */
|
|
451
|
+
export function sketchSource(value: Sketch): Sketch | undefined {
|
|
452
|
+
return (value as SketchValue).source;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** The relation frame used by this sketch and inherited by its generated faces. */
|
|
456
|
+
export function sketchFrame(value: Sketch): SketchFrame {
|
|
457
|
+
return (value as SketchValue).frame;
|
|
458
|
+
}
|
|
459
|
+
|
|
405
460
|
export type SketchPointSnapshot = Readonly<{
|
|
406
461
|
kind: 'point';
|
|
407
462
|
id: number;
|
|
@@ -497,17 +552,30 @@ export function snapshotSketch(
|
|
|
497
552
|
redundant,
|
|
498
553
|
} = sketchDefinition(value);
|
|
499
554
|
const id = identity(value);
|
|
555
|
+
const ancestors: Sketch[] = [];
|
|
556
|
+
for (
|
|
557
|
+
let ancestor = base;
|
|
558
|
+
ancestor;
|
|
559
|
+
ancestor = sketchDefinition(ancestor).base
|
|
560
|
+
)
|
|
561
|
+
ancestors.push(ancestor);
|
|
562
|
+
const pointIdentity = (reference: Sketch) =>
|
|
563
|
+
identity(
|
|
564
|
+
ancestors.find(
|
|
565
|
+
ancestor => definitions.get(ancestor) === definitions.get(reference),
|
|
566
|
+
) ?? reference,
|
|
567
|
+
);
|
|
500
568
|
return {
|
|
501
569
|
id,
|
|
502
570
|
base: base && identity(base),
|
|
503
|
-
entities: snapshotEntries(entries, id,
|
|
571
|
+
entities: snapshotEntries(entries, id, pointIdentity, points).map(e =>
|
|
504
572
|
e.kind === 'point'
|
|
505
573
|
? {...e, position: points.get(e.id)!}
|
|
506
574
|
: e.kind === 'circle' || e.kind === 'arc'
|
|
507
575
|
? {...e, radius: radii.get(e.id)!}
|
|
508
576
|
: e,
|
|
509
577
|
),
|
|
510
|
-
constraints: snapshotConstraints(constraints, id,
|
|
578
|
+
constraints: snapshotConstraints(constraints, id, pointIdentity),
|
|
511
579
|
degreesOfFreedom,
|
|
512
580
|
redundant,
|
|
513
581
|
};
|
|
@@ -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
|
+
}
|