@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.
Files changed (128) hide show
  1. package/README.md +223 -786
  2. package/THIRD_PARTY.md +40 -0
  3. package/bld/THIRD_PARTY_NOTICES.txt +182 -0
  4. package/bld/chunks/chunk-5E6VXJU2.js +30 -0
  5. package/bld/chunks/chunk-5E6VXJU2.js.map +7 -0
  6. package/bld/chunks/chunk-63ZUQWQ2.js +19 -0
  7. package/bld/chunks/chunk-63ZUQWQ2.js.map +7 -0
  8. package/bld/chunks/chunk-AWYI64EE.js +31 -0
  9. package/bld/chunks/chunk-AWYI64EE.js.map +7 -0
  10. package/bld/chunks/chunk-F53RTQPT.js +24787 -0
  11. package/bld/chunks/chunk-F53RTQPT.js.map +7 -0
  12. package/bld/chunks/chunk-TJIN6S4R.js +1 -0
  13. package/bld/chunks/chunk-TJIN6S4R.js.map +7 -0
  14. package/bld/library/cached.d.ts +33 -0
  15. package/bld/library/cached.d.ts.map +1 -0
  16. package/bld/library/font.d.ts +39 -0
  17. package/bld/library/font.d.ts.map +1 -0
  18. package/bld/library/google-font.d.ts +14 -0
  19. package/bld/library/google-font.d.ts.map +1 -0
  20. package/bld/library/index.d.ts +8 -2
  21. package/bld/library/index.d.ts.map +1 -1
  22. package/bld/library/index.js +60 -3
  23. package/bld/library/index.js.map +7 -1
  24. package/bld/library/kernel-artifact-codec.d.ts.map +1 -1
  25. package/bld/library/kernel-cache.d.ts +17 -6
  26. package/bld/library/kernel-cache.d.ts.map +1 -1
  27. package/bld/library/material.d.ts +1 -1
  28. package/bld/library/material.d.ts.map +1 -1
  29. package/bld/library/open-cascade.d.ts +4 -0
  30. package/bld/library/open-cascade.d.ts.map +1 -0
  31. package/bld/library/replicad.d.ts +3 -2
  32. package/bld/library/replicad.d.ts.map +1 -1
  33. package/bld/library/replicad.js +10 -24
  34. package/bld/library/replicad.js.map +7 -1
  35. package/bld/library/retained-memory.d.ts.map +1 -1
  36. package/bld/library/runtime.d.ts +144 -80
  37. package/bld/library/runtime.d.ts.map +1 -1
  38. package/bld/library/sketch-solver.d.ts +1 -1
  39. package/bld/library/sketch-solver.d.ts.map +1 -1
  40. package/bld/library/sketch.d.ts +9 -1
  41. package/bld/library/sketch.d.ts.map +1 -1
  42. package/bld/library/text.d.ts +53 -0
  43. package/bld/library/text.d.ts.map +1 -0
  44. package/bld/library/three.js +3 -3
  45. package/bld/library/three.js.map +7 -1
  46. package/bld/node/index.d.ts.map +1 -1
  47. package/bld/node/index.js +62 -14
  48. package/bld/node/index.js.map +7 -1
  49. package/bld/node/replicad.js +13 -3
  50. package/bld/node/replicad.js.map +7 -1
  51. package/bld/tooling/index.d.ts +17 -15
  52. package/bld/tooling/index.d.ts.map +1 -1
  53. package/bld/tooling/index.js +147 -19
  54. package/bld/tooling/index.js.map +7 -1
  55. package/package.json +18 -6
  56. package/src/library/cached.ts +132 -0
  57. package/src/library/font.ts +186 -0
  58. package/src/library/google-font.ts +97 -0
  59. package/src/library/index.ts +8 -3
  60. package/src/library/kernel-artifact-codec.ts +129 -20
  61. package/src/library/kernel-cache.ts +111 -33
  62. package/src/library/material.ts +3 -4
  63. package/src/library/open-cascade.ts +13 -0
  64. package/src/library/replicad.ts +5 -5
  65. package/src/library/retained-memory.ts +6 -1
  66. package/src/library/runtime.ts +2049 -1728
  67. package/src/library/sketch-solver.ts +3 -1
  68. package/src/library/sketch.ts +81 -13
  69. package/src/library/text.ts +477 -0
  70. package/src/library/tsconfig.json +2 -1
  71. package/src/node/index.ts +14 -3
  72. package/src/node/tsconfig.json +2 -1
  73. package/src/tooling/index.ts +48 -46
  74. package/src/tooling/tsconfig.json +2 -1
  75. package/bld/library/alignment-geometry.js +0 -322
  76. package/bld/library/alignment-geometry.js.map +0 -1
  77. package/bld/library/bound-solver.js +0 -252
  78. package/bld/library/bound-solver.js.map +0 -1
  79. package/bld/library/extrude.js +0 -28
  80. package/bld/library/extrude.js.map +0 -1
  81. package/bld/library/kernel-artifact-codec.js +0 -148
  82. package/bld/library/kernel-artifact-codec.js.map +0 -1
  83. package/bld/library/kernel-cache.js +0 -245
  84. package/bld/library/kernel-cache.js.map +0 -1
  85. package/bld/library/kernel-shapes.js +0 -108
  86. package/bld/library/kernel-shapes.js.map +0 -1
  87. package/bld/library/loft.js +0 -161
  88. package/bld/library/loft.js.map +0 -1
  89. package/bld/library/material.js +0 -176
  90. package/bld/library/material.js.map +0 -1
  91. package/bld/library/model-color.js +0 -36
  92. package/bld/library/model-color.js.map +0 -1
  93. package/bld/library/open-cascade-error.js +0 -22
  94. package/bld/library/open-cascade-error.js.map +0 -1
  95. package/bld/library/relation-solver.js +0 -258
  96. package/bld/library/relation-solver.js.map +0 -1
  97. package/bld/library/retained-memory.js +0 -27
  98. package/bld/library/retained-memory.js.map +0 -1
  99. package/bld/library/runtime.js +0 -2954
  100. package/bld/library/runtime.js.map +0 -1
  101. package/bld/library/shell.js +0 -221
  102. package/bld/library/shell.js.map +0 -1
  103. package/bld/library/sketch-curve-intersections.js +0 -117
  104. package/bld/library/sketch-curve-intersections.js.map +0 -1
  105. package/bld/library/sketch-curves.js +0 -89
  106. package/bld/library/sketch-curves.js.map +0 -1
  107. package/bld/library/sketch-drag-rules.js +0 -522
  108. package/bld/library/sketch-drag-rules.js.map +0 -1
  109. package/bld/library/sketch-face.js +0 -72
  110. package/bld/library/sketch-face.js.map +0 -1
  111. package/bld/library/sketch-incidence.js +0 -114
  112. package/bld/library/sketch-incidence.js.map +0 -1
  113. package/bld/library/sketch-precision.js +0 -55
  114. package/bld/library/sketch-precision.js.map +0 -1
  115. package/bld/library/sketch-regions.js +0 -193
  116. package/bld/library/sketch-regions.js.map +0 -1
  117. package/bld/library/sketch-solver.js +0 -578
  118. package/bld/library/sketch-solver.js.map +0 -1
  119. package/bld/library/sketch.js +0 -650
  120. package/bld/library/sketch.js.map +0 -1
  121. package/bld/library/spatial.js +0 -195
  122. package/bld/library/spatial.js.map +0 -1
  123. package/bld/library/topology-id.js +0 -67
  124. package/bld/library/topology-id.js.map +0 -1
  125. package/bld/library/topology-inspection.js +0 -267
  126. package/bld/library/topology-inspection.js.map +0 -1
  127. package/bld/library/topology.js +0 -455
  128. package/bld/library/topology.js.map +0 -1
@@ -0,0 +1,132 @@
1
+ import {encodeKernelArtifact} from './kernel-artifact-codec.js';
2
+ import {
3
+ acceptKernelOperation,
4
+ evaluateCachedArtifact,
5
+ findKernelOperation,
6
+ findKernelOperations,
7
+ kernelContentId,
8
+ kernelOperationKey,
9
+ type CacheCodec,
10
+ type KernelArtifact,
11
+ type KernelOperationKey,
12
+ type KernelValueLifecycle,
13
+ } from './kernel-cache.js';
14
+ import {estimateRetainedBytes} from './retained-memory.js';
15
+
16
+ export type CachedOptions<Value> = CacheCodec<Value>;
17
+
18
+ const identities = new WeakMap<Function, string>();
19
+ const sessionIdentities = new WeakMap<Function, string>();
20
+ const session = crypto.randomUUID();
21
+ let nextIdentity = 0;
22
+
23
+ /** The compiler supplies the definition and its static dependency fingerprint. */
24
+ export function identifyCachedFunction<Fn extends Function>(
25
+ fn: Fn,
26
+ identity: string,
27
+ ): Fn {
28
+ // Identity belongs to this cache definition (including its codec), not to
29
+ // every other use of the author's original function object.
30
+ const identified = function (this: unknown, ...args: unknown[]) {
31
+ return Reflect.apply(fn, this, args);
32
+ } as unknown as Fn;
33
+ identities.set(identified, identity);
34
+ return identified;
35
+ }
36
+
37
+ function functionIdentity(fn: Function): {id: string; persistent: boolean} {
38
+ const identity = identities.get(fn);
39
+ if (identity) return {id: identity, persistent: true};
40
+ let id = sessionIdentities.get(fn);
41
+ if (!id) {
42
+ id = `${session}:${nextIdentity++}`;
43
+ sessionIdentities.set(fn, id);
44
+ }
45
+ // Without compiler fingerprints, object identity is the only sound identity
46
+ // for a JavaScript closure. It still shares the process-wide memory budget.
47
+ return {id, persistent: false};
48
+ }
49
+
50
+ const dataLifecycle: KernelValueLifecycle<unknown> = {
51
+ estimateBytes: estimateRetainedBytes,
52
+ retain: value => value,
53
+ instantiate: value => value,
54
+ release() {},
55
+ };
56
+
57
+ /**
58
+ * Memoizes a synchronous, deterministic computation in the shared cache.
59
+ * Pass changing captured state as arguments and treat returned data as immutable.
60
+ * The engine fingerprints definitions and dependencies for persistent reuse.
61
+ * Outside the engine, function identity provides process-local memory reuse.
62
+ * Custom encoder/decoder pairs run only when writing/restoring persistent data;
63
+ * memory hits return the retained value without decoding or copying it.
64
+ */
65
+ export function cached<Args extends unknown[], Value>(
66
+ compute: (
67
+ ...args: Args
68
+ ) => Value & (Value extends PromiseLike<unknown> ? never : unknown),
69
+ options?: CachedOptions<NoInfer<Value>>,
70
+ ): (...args: Args) => Value {
71
+ const operation = cachedArtifact(
72
+ (...args: Args): Value => {
73
+ const value = compute(...args);
74
+ if (
75
+ value &&
76
+ typeof value === 'object' &&
77
+ 'then' in value &&
78
+ typeof value.then === 'function'
79
+ )
80
+ throw new Error('cached() requires a synchronous computation.');
81
+ return value;
82
+ },
83
+ {identity: compute, codec: options},
84
+ );
85
+ return (...args) => operation(...args).value;
86
+ }
87
+
88
+ /** Internal artifact form also supports native ownership and remote admission. */
89
+ export function cachedArtifact<Args extends unknown[], Value>(
90
+ compute: (...args: Args) => Value,
91
+ {
92
+ key,
93
+ lifecycle = dataLifecycle as KernelValueLifecycle<Value>,
94
+ codec,
95
+ identity = compute,
96
+ namespace = 'cached',
97
+ }: {
98
+ key?: (...args: Args) => KernelOperationKey;
99
+ lifecycle?: KernelValueLifecycle<Value>;
100
+ codec?: CacheCodec<Value> | false;
101
+ identity?: Function;
102
+ namespace?: string;
103
+ } = {},
104
+ ) {
105
+ const definition = functionIdentity(identity);
106
+ const persistence = key || definition.persistent ? codec : false;
107
+ const operationKey =
108
+ key ??
109
+ ((...args: Args) =>
110
+ kernelOperationKey(
111
+ namespace,
112
+ [definition.id, kernelContentId(encodeKernelArtifact('', args))],
113
+ [],
114
+ ));
115
+ const find = (key: KernelOperationKey) =>
116
+ findKernelOperation(key, lifecycle, persistence);
117
+ const findMany = (keys: readonly KernelOperationKey[]) =>
118
+ findKernelOperations(keys, lifecycle, persistence);
119
+ const accept = (key: KernelOperationKey, value: Value) =>
120
+ acceptKernelOperation(key, lifecycle, value, persistence);
121
+ return Object.assign(
122
+ (...args: Args): KernelArtifact<Value> => {
123
+ return evaluateCachedArtifact(
124
+ operationKey(...args),
125
+ lifecycle,
126
+ () => compute(...args),
127
+ persistence,
128
+ );
129
+ },
130
+ {key: operationKey, find, findMany, accept},
131
+ );
132
+ }
@@ -0,0 +1,186 @@
1
+ /// <reference types="emscripten" preserve="true" />
2
+ import {cachedArtifact} from './cached.js';
3
+ import type * as HarfBuzz from 'harfbuzzjs';
4
+ import {
5
+ googleFontSources,
6
+ googleFontUrl,
7
+ type GoogleFontOptions,
8
+ } from './google-font.js';
9
+ import {
10
+ kernelOperationKey,
11
+ kernelContentId,
12
+ type KernelArtifact,
13
+ } from './kernel-cache.js';
14
+
15
+ const fontBrand: unique symbol = Symbol('Font');
16
+ /** An immutable, content-addressed font resource. Size is supplied to text(). */
17
+ export type Font = Readonly<{
18
+ family: string;
19
+ style: string;
20
+ [fontBrand]: true;
21
+ }>;
22
+ export type ParsedFont = Readonly<{
23
+ face: HarfBuzz.Face;
24
+ font: HarfBuzz.Font;
25
+ numGlyphs: number;
26
+ sourceBytes: number;
27
+ }>;
28
+ let fontEngine: typeof HarfBuzz;
29
+ let shapingBuffer: HarfBuzz.Buffer;
30
+ export function installFontEngine(engine: typeof HarfBuzz): void {
31
+ fontEngine = engine;
32
+ shapingBuffer = new engine.Buffer();
33
+ }
34
+ export function shapeFontText(
35
+ font: HarfBuzz.Font,
36
+ content: string,
37
+ kerning: boolean,
38
+ ) {
39
+ shapingBuffer.reset();
40
+ shapingBuffer.addText(content);
41
+ shapingBuffer.guessSegmentProperties();
42
+ fontEngine.shape(font, shapingBuffer, [
43
+ new fontEngine.Feature('kern', Number(kerning)),
44
+ ]);
45
+ return {
46
+ infos: shapingBuffer.getGlyphInfos(),
47
+ positions: shapingBuffer.getGlyphPositions(),
48
+ };
49
+ }
50
+ export type FontPart = Readonly<{
51
+ artifact: KernelArtifact<ParsedFont>;
52
+ ranges: readonly (readonly [number, number])[];
53
+ }>;
54
+ const fonts = new WeakMap<Font, readonly FontPart[]>();
55
+ let readResource: ((url: URL) => Uint8Array | undefined) | undefined;
56
+
57
+ /** The model engine prepares resources before synchronous author code runs. */
58
+ export function installModelResourceReader(reader: typeof readResource): void {
59
+ readResource = reader;
60
+ }
61
+
62
+ /** Reads font bytes or a prepared project/HTTP(S) URL; Node also reads file URLs. */
63
+ export function font(source: URL | ArrayBuffer | Uint8Array): Font {
64
+ return fontValue([{artifact: parseFont(source), ranges: []}]);
65
+ }
66
+
67
+ /**
68
+ * A Google Fonts family/style prepared by the model engine before execution.
69
+ * @modelResource google-font
70
+ */
71
+ export function googleFont(
72
+ family: string,
73
+ options: GoogleFontOptions = {},
74
+ ): Font {
75
+ const url = googleFontUrl(family, options);
76
+ const css = readResource?.(url);
77
+ if (!css)
78
+ throw new Error(
79
+ 'googleFont() requires resources prepared by the model engine. Use a static family name and options.',
80
+ );
81
+ return fontValue(
82
+ googleFontSources(css).map(({url, ranges}) => ({
83
+ artifact: parseFont(new URL(url), options),
84
+ ranges,
85
+ })),
86
+ );
87
+ }
88
+
89
+ function parseFont(
90
+ source: URL | ArrayBuffer | Uint8Array,
91
+ options: GoogleFontOptions = {},
92
+ ): KernelArtifact<ParsedFont> {
93
+ if (!fontEngine) throw new Error('The font engine has not been initialized.');
94
+ const bytes =
95
+ source instanceof URL
96
+ ? readResource?.(source)
97
+ : source instanceof ArrayBuffer
98
+ ? new Uint8Array(source)
99
+ : source;
100
+ if (!(bytes instanceof Uint8Array)) {
101
+ throw new Error(
102
+ 'font() requires font bytes or a URL prepared by the model engine. Use a static new URL("./font.ttf", import.meta.url) or new URL("https://…/font.ttf"); outside the engine, fetch the font first and pass its bytes.',
103
+ );
104
+ }
105
+ return parsedFont(bytes, options);
106
+ }
107
+
108
+ const parsedFont = cachedArtifact(
109
+ (bytes: Uint8Array, options: GoogleFontOptions): ParsedFont => {
110
+ try {
111
+ const signature = new DataView(
112
+ bytes.buffer,
113
+ bytes.byteOffset,
114
+ bytes.byteLength,
115
+ ).getUint32(0);
116
+ if (signature !== 0x00010000 && signature !== 0x4f54544f)
117
+ throw new Error('Expected an SFNT font.');
118
+ const face = new fontEngine.Face(new fontEngine.Blob(bytes));
119
+ const maxp = face.referenceTable('maxp');
120
+ if (!maxp || maxp.byteLength < 6 || !face.referenceTable('cmap'))
121
+ throw new Error('Missing font tables.');
122
+ const numGlyphs = new DataView(
123
+ maxp.buffer,
124
+ maxp.byteOffset,
125
+ maxp.byteLength,
126
+ ).getUint16(4);
127
+ if (!numGlyphs) throw new Error('Font has no glyphs.');
128
+ const font = new fontEngine.Font(face);
129
+ const variations: HarfBuzz.Variation[] = [];
130
+ if (options.weight !== undefined)
131
+ variations.push(new fontEngine.Variation('wght', options.weight));
132
+ if (options.italic !== undefined)
133
+ variations.push(
134
+ new fontEngine.Variation('ital', Number(options.italic)),
135
+ );
136
+ font.setVariations(variations);
137
+ return {face, font, numGlyphs, sourceBytes: bytes.byteLength};
138
+ } catch (error) {
139
+ throw new Error(
140
+ 'Cannot parse font. Expected a TTF or OTF font (font collections and WOFF2 are not supported).',
141
+ {cause: error},
142
+ );
143
+ }
144
+ },
145
+ {
146
+ key: (bytes, options) =>
147
+ kernelOperationKey(
148
+ 'font',
149
+ [
150
+ kernelContentId(bytes),
151
+ bytes.byteLength,
152
+ options.weight ?? null,
153
+ options.italic ?? null,
154
+ ],
155
+ [],
156
+ ),
157
+ codec: false,
158
+ lifecycle: {
159
+ // Account for parser tables and lazy glyph expansion in the shared budget.
160
+ estimateBytes: value => value.sourceBytes * 8 + value.numGlyphs * 1024,
161
+ retain: value => value,
162
+ instantiate: value => value,
163
+ release() {},
164
+ },
165
+ },
166
+ );
167
+
168
+ function fontValue(parts: readonly FontPart[]): Font {
169
+ const parsed = parts[0].artifact.value;
170
+ const value: Font = Object.freeze({
171
+ family: parsed.face.getName(1, 'en'),
172
+ style: parsed.face.getName(2, 'en'),
173
+ [fontBrand]: true as const,
174
+ });
175
+ fonts.set(value, parts);
176
+ return value;
177
+ }
178
+
179
+ export function fontParts(value: Font): readonly FontPart[] {
180
+ const parts = fonts.get(value);
181
+ if (!parts)
182
+ throw new Error(
183
+ 'text() requires a font created by font() or googleFont().',
184
+ );
185
+ return parts;
186
+ }
@@ -0,0 +1,97 @@
1
+ import {cachedArtifact} from './cached.js';
2
+ import {kernelContentId, kernelOperationKey} from './kernel-cache.js';
3
+
4
+ export type GoogleFontOptions = Readonly<{
5
+ /** Omit to use Google Fonts' default weight. */
6
+ weight?: number;
7
+ /** Omit to use Google Fonts' default style. */
8
+ italic?: boolean;
9
+ }>;
10
+
11
+ export type GoogleFontSource = Readonly<{
12
+ url: string;
13
+ ranges: readonly (readonly [number, number])[];
14
+ }>;
15
+
16
+ /** Canonical request shared by synchronous Core and asynchronous preparation. */
17
+ export function googleFontUrl(
18
+ family: string,
19
+ options: GoogleFontOptions = {},
20
+ ): URL {
21
+ if (typeof family !== 'string' || !family.trim())
22
+ throw new Error('Google font family must be a non-empty name.');
23
+ if (
24
+ options.weight !== undefined &&
25
+ (!Number.isFinite(options.weight) ||
26
+ options.weight < 1 ||
27
+ options.weight > 1000)
28
+ )
29
+ throw new Error('Google font weight must be between 1 and 1000.');
30
+ if (options.italic !== undefined && typeof options.italic !== 'boolean')
31
+ throw new Error('Google font italic must be a boolean.');
32
+ const axes: [string, number][] = [];
33
+ if (options.italic !== undefined) axes.push(['ital', Number(options.italic)]);
34
+ if (options.weight !== undefined) axes.push(['wght', options.weight]);
35
+ const specification =
36
+ family.trim() +
37
+ (axes.length
38
+ ? ':' +
39
+ axes.map(([axis]) => axis).join(',') +
40
+ '@' +
41
+ axes.map(([, value]) => value).join(',')
42
+ : '');
43
+ return new URL(
44
+ 'https://fonts.googleapis.com/css2?' +
45
+ new URLSearchParams({family: specification}),
46
+ );
47
+ }
48
+
49
+ /** Parse only font-face resource declarations; no stylesheet is installed. */
50
+ const parseGoogleFontSources = cachedArtifact(
51
+ (bytes: Uint8Array): readonly GoogleFontSource[] => {
52
+ const css = new TextDecoder()
53
+ .decode(bytes)
54
+ .replace(/\/\*[\s\S]*?\*\//g, '');
55
+ const sources: GoogleFontSource[] = [];
56
+ for (const [, body] of css.matchAll(/@font-face\s*\{([^}]*)\}/gi)) {
57
+ const source = /(?:^|;)\s*src\s*:\s*([^;]+)/i.exec(body)?.[1];
58
+ const rawUrl =
59
+ source &&
60
+ /url\(\s*(?:"([^"]+)"|'([^']+)'|([^\s)]+))\s*\)/i.exec(source);
61
+ if (!rawUrl)
62
+ throw new Error('Google Fonts returned a font face without a URL.');
63
+ const url = new URL(rawUrl[1] ?? rawUrl[2] ?? rawUrl[3]);
64
+ if (url.protocol !== 'https:')
65
+ throw new Error('Google Fonts returned a non-HTTPS font URL.');
66
+ const ranges: [number, number][] = [];
67
+ const range = /(?:^|;)\s*unicode-range\s*:\s*([^;]+)/i.exec(body)?.[1];
68
+ if (range)
69
+ for (const part of range.split(',')) {
70
+ const match =
71
+ /^\s*U\+([0-9a-f?]{1,6})(?:-([0-9a-f]{1,6}))?\s*$/i.exec(part);
72
+ if (!match)
73
+ throw new Error('Google Fonts returned an invalid Unicode range.');
74
+ const start = parseInt(match[1].replaceAll('?', '0'), 16);
75
+ const end = parseInt(match[2] ?? match[1].replaceAll('?', 'f'), 16);
76
+ if (start > end || end > 0x10ffff)
77
+ throw new Error('Google Fonts returned an invalid Unicode range.');
78
+ ranges.push([start, end]);
79
+ }
80
+ sources.push({url: url.href, ranges});
81
+ }
82
+ if (!sources.length)
83
+ throw new Error('Google Fonts returned no font files.');
84
+ // Later CSS faces take precedence where Unicode ranges overlap.
85
+ return sources.reverse();
86
+ },
87
+ {
88
+ key: bytes =>
89
+ kernelOperationKey('googleFontSources', [kernelContentId(bytes)], []),
90
+ },
91
+ );
92
+
93
+ export function googleFontSources(
94
+ bytes: Uint8Array,
95
+ ): readonly GoogleFontSource[] {
96
+ return parseGoogleFontSources(bytes).value;
97
+ }
@@ -1,3 +1,9 @@
1
+ export {cached} from './cached.js';
2
+ export type {CachedOptions} from './cached.js';
3
+ export {font, googleFont} from './font.js';
4
+ export type {Font} from './font.js';
5
+ export type {GoogleFontOptions} from './google-font.js';
6
+ export type {TextOptions} from './text.js';
1
7
  export {sketch} from './sketch.js';
2
8
  export type {
3
9
  Sketch,
@@ -19,6 +25,7 @@ export {
19
25
  cylinder,
20
26
  ellipse,
21
27
  extrude,
28
+ text,
22
29
  frustum,
23
30
  group,
24
31
  intersect,
@@ -61,9 +68,7 @@ export type {
61
68
  Model,
62
69
  ModelCapabilities,
63
70
  ModelElementKind,
64
- ModelFamily,
65
- ModelFamilyElementKind,
66
- ModelForFamily,
71
+ ModelForKind,
67
72
  ModelGeometryKind,
68
73
  ModelKind,
69
74
  NamedElements,
@@ -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');