@selvajs/visualization 1.6.0-beta.1 → 1.6.0-beta.11

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 (43) hide show
  1. package/dist/gpu-dispose-BKp7iijv.js +355 -0
  2. package/dist/gpu-dispose-BKp7iijv.js.map +1 -0
  3. package/dist/gpu-dispose-Cxu3dFmA.cjs +469 -0
  4. package/dist/gpu-dispose-Cxu3dFmA.cjs.map +1 -0
  5. package/dist/gpu-ownership-ADRZBxxs.cjs +102 -0
  6. package/dist/gpu-ownership-ADRZBxxs.cjs.map +1 -0
  7. package/dist/gpu-ownership-Bs9l7hrU.js +40 -0
  8. package/dist/gpu-ownership-Bs9l7hrU.js.map +1 -0
  9. package/dist/index.js +1 -1
  10. package/dist/merged-draw-CO4rr2Co.js +129 -0
  11. package/dist/merged-draw-CO4rr2Co.js.map +1 -0
  12. package/dist/merged-draw-ma_2r4N8.cjs +171 -0
  13. package/dist/merged-draw-ma_2r4N8.cjs.map +1 -0
  14. package/dist/parse.cjs +2789 -11
  15. package/dist/parse.cjs.map +1 -1
  16. package/dist/parse.d.cts +15 -18
  17. package/dist/parse.d.ts +15 -18
  18. package/dist/parse.js +2780 -11
  19. package/dist/parse.js.map +1 -1
  20. package/dist/render.cjs +4302 -6
  21. package/dist/render.cjs.map +1 -1
  22. package/dist/render.d.cts +62 -43
  23. package/dist/render.d.ts +62 -43
  24. package/dist/render.js +4281 -6
  25. package/dist/render.js.map +1 -1
  26. package/dist/scene.cjs +343 -1
  27. package/dist/scene.cjs.map +1 -1
  28. package/dist/scene.d.cts +0 -12
  29. package/dist/scene.d.ts +0 -12
  30. package/dist/scene.js +336 -1
  31. package/dist/scene.js.map +1 -1
  32. package/dist/{types-DjPelPRM.d.ts → types-DW_qU4-Y.d.cts} +22 -5
  33. package/dist/{types-DjPelPRM.d.cts → types-DW_qU4-Y.d.ts} +22 -5
  34. package/package.json +18 -11
  35. package/dist/gpu-dispose-Dich0oW7.js +0 -2
  36. package/dist/gpu-dispose-Dich0oW7.js.map +0 -1
  37. package/dist/gpu-dispose-GZmNW-gK.cjs +0 -2
  38. package/dist/gpu-dispose-GZmNW-gK.cjs.map +0 -1
  39. package/dist/merged-draw-B75jHIzV.cjs +0 -2
  40. package/dist/merged-draw-B75jHIzV.cjs.map +0 -1
  41. package/dist/merged-draw-BPhBYH-v.js +0 -2
  42. package/dist/merged-draw-BPhBYH-v.js.map +0 -1
  43. package/dist/rolldown-runtime-BocRIvOZ.cjs +0 -1
@@ -0,0 +1,469 @@
1
+ const require_gpu_ownership = require("./gpu-ownership-ADRZBxxs.cjs");
2
+ let three = require("three");
3
+ three = require_gpu_ownership.__toESM(three, 1);
4
+ // src/shared/errors.ts
5
+ /**
6
+ * Errors for the visualization package. Replaces `@selvajs/compute`'s `RhinoComputeError`, which
7
+ * mis-named failures on paths (e.g. the plugin WebSocket) that never touch Rhino.Compute. `code`
8
+ * values match compute's so existing catch-sites keep working.
9
+ */
10
+ const ErrorCodes = {
11
+ /** Structural check failed: bad magic bytes, out-of-window index, malformed metadata. */
12
+ VALIDATION_ERROR: "VALIDATION_ERROR",
13
+ INVALID_STATE: "INVALID_STATE",
14
+ /** No `DecompressionStream`, no WebGL context, etc. */
15
+ ENVIRONMENT_ERROR: "ENVIRONMENT_ERROR",
16
+ INVALID_CONFIG: "INVALID_CONFIG",
17
+ /** Base64 input could not be decoded. */
18
+ ENCODING_ERROR: "ENCODING_ERROR",
19
+ UNKNOWN_ERROR: "UNKNOWN_ERROR"
20
+ };
21
+ var VisualizationError = class extends Error {
22
+ code;
23
+ context;
24
+ originalError;
25
+ constructor(message, code = ErrorCodes.UNKNOWN_ERROR, options) {
26
+ super(message);
27
+ this.name = "VisualizationError";
28
+ this.code = code;
29
+ this.context = options?.context;
30
+ this.originalError = options?.originalError;
31
+ if (options?.originalError) this.cause = options.originalError;
32
+ }
33
+ };
34
+ // src/shared/logger.ts
35
+ /**
36
+ * Logging facility for the visualization package. Deliberately local rather than imported from
37
+ * `@selvajs/compute` (logging isn't a compute concern). Mirrors compute's logger shape so a host
38
+ * wanting one sink for both can call `setLogger(computeLogger.getLogger())`.
39
+ */
40
+ var NoOpLogger = class {
41
+ debug() {}
42
+ info() {}
43
+ warn() {}
44
+ error() {}
45
+ };
46
+ var ConsoleLogger = class {
47
+ debug(message, ...args) {
48
+ console.debug(message, ...args);
49
+ }
50
+ info(message, ...args) {
51
+ console.info(message, ...args);
52
+ }
53
+ warn(message, ...args) {
54
+ console.warn(message, ...args);
55
+ }
56
+ error(message, ...args) {
57
+ console.error(message, ...args);
58
+ }
59
+ };
60
+ let internalLogger = new NoOpLogger();
61
+ function getLogger() {
62
+ return internalLogger;
63
+ }
64
+ function setLogger(logger) {
65
+ if (logger === null) {
66
+ internalLogger = new NoOpLogger();
67
+ return;
68
+ }
69
+ const missing = [
70
+ "debug",
71
+ "info",
72
+ "warn",
73
+ "error"
74
+ ].filter((method) => typeof logger[method] !== "function");
75
+ if (missing.length > 0) throw new VisualizationError(`Logger is missing required method(s): ${missing.join(", ")}. A logger must implement debug, info, warn and error.`, ErrorCodes.INVALID_CONFIG, { context: { missingMethods: missing } });
76
+ internalLogger = logger;
77
+ }
78
+ function enableDebugLogging() {
79
+ setLogger(new ConsoleLogger());
80
+ }
81
+ // src/shared/looks.ts
82
+ /** The look applied when the caller passes no `look` option. */
83
+ const DEFAULT_LOOK = "technical";
84
+ /**
85
+ * Single source of truth for both `applyDefaults` (construction) and `setLook` (runtime), so the two
86
+ * can't drift.
87
+ *
88
+ * Every shaded look pairs a key light with contact shading (GTAO), because image-based lighting on
89
+ * its own lights all faces of a box nearly equally — the result is a white silhouette with no
90
+ * readable form. GTAO is a full-screen pass and does cost frames; `xray` skips it since there are no
91
+ * opaque contacts left to shade.
92
+ */
93
+ const LOOK_PRESETS = {
94
+ studio: {
95
+ toneMapping: three.ACESFilmicToneMapping,
96
+ toneMappingExposure: 1,
97
+ envMapIntensity: 1,
98
+ environmentIntensity: .85,
99
+ hemisphereIntensity: .5,
100
+ ambientIntensity: .25,
101
+ cullBackfaces: false,
102
+ ambientOcclusion: true,
103
+ sunlightIntensity: 1.6
104
+ },
105
+ technical: {
106
+ toneMapping: three.NeutralToneMapping,
107
+ toneMappingExposure: 1,
108
+ envMapIntensity: .55,
109
+ environmentIntensity: .6,
110
+ hemisphereIntensity: .25,
111
+ ambientIntensity: .15,
112
+ cullBackfaces: false,
113
+ ambientOcclusion: true,
114
+ sunlightIntensity: 2.2
115
+ },
116
+ showcase: {
117
+ toneMapping: three.ACESFilmicToneMapping,
118
+ toneMappingExposure: 1.15,
119
+ envMapIntensity: 1.4,
120
+ environmentIntensity: 1,
121
+ hemisphereIntensity: .2,
122
+ ambientIntensity: .1,
123
+ cullBackfaces: false,
124
+ ambientOcclusion: true,
125
+ sunlightIntensity: 2.6
126
+ },
127
+ arctic: {
128
+ toneMapping: three.NeutralToneMapping,
129
+ toneMappingExposure: 1.05,
130
+ envMapIntensity: .85,
131
+ environmentIntensity: 1.1,
132
+ hemisphereIntensity: .3,
133
+ ambientIntensity: .15,
134
+ cullBackfaces: false,
135
+ ambientOcclusion: true,
136
+ sunlightIntensity: 2,
137
+ materialOverride: {
138
+ color: 15659509,
139
+ metalness: 0,
140
+ roughness: .75,
141
+ emissiveIntensity: 0
142
+ }
143
+ },
144
+ xray: {
145
+ toneMapping: three.NeutralToneMapping,
146
+ toneMappingExposure: 1.3,
147
+ envMapIntensity: .4,
148
+ environmentIntensity: .8,
149
+ hemisphereIntensity: .6,
150
+ ambientIntensity: .6,
151
+ cullBackfaces: false,
152
+ ambientOcclusion: false,
153
+ sunlightIntensity: .6,
154
+ materialOverride: {
155
+ color: 10470632,
156
+ metalness: 0,
157
+ roughness: .9,
158
+ emissiveIntensity: 0,
159
+ opacity: .28,
160
+ depthWrite: false,
161
+ side: three.DoubleSide
162
+ }
163
+ },
164
+ lineart: {
165
+ toneMapping: three.NeutralToneMapping,
166
+ toneMappingExposure: 1,
167
+ envMapIntensity: 1,
168
+ environmentIntensity: 1,
169
+ hemisphereIntensity: .6,
170
+ ambientIntensity: 3.2,
171
+ cullBackfaces: false,
172
+ ambientOcclusion: false,
173
+ sunlightIntensity: 0,
174
+ requiresEdges: true,
175
+ materialOverride: {
176
+ color: 16251130,
177
+ metalness: 0,
178
+ roughness: 1,
179
+ emissiveIntensity: 0
180
+ }
181
+ },
182
+ night: {
183
+ toneMapping: three.ACESFilmicToneMapping,
184
+ toneMappingExposure: 1,
185
+ envMapIntensity: .32,
186
+ environmentIntensity: .15,
187
+ hemisphereIntensity: .26,
188
+ ambientIntensity: .05,
189
+ cullBackfaces: false,
190
+ ambientOcclusion: true,
191
+ sunlightIntensity: .14,
192
+ background: 658962,
193
+ bloom: {
194
+ strength: .2,
195
+ radius: .3,
196
+ threshold: 1.8
197
+ },
198
+ emissiveLights: true,
199
+ lowPower: {
200
+ ambientOcclusion: false,
201
+ bloom: void 0,
202
+ maxEmissiveLights: 1,
203
+ maxPixelRatio: 1.5
204
+ }
205
+ },
206
+ wireframe: {
207
+ toneMapping: three.NeutralToneMapping,
208
+ toneMappingExposure: 1,
209
+ envMapIntensity: 1,
210
+ environmentIntensity: 1,
211
+ hemisphereIntensity: .4,
212
+ ambientIntensity: 2,
213
+ cullBackfaces: false,
214
+ ambientOcclusion: false,
215
+ sunlightIntensity: 0,
216
+ materialOverride: {
217
+ color: 2830648,
218
+ metalness: 0,
219
+ roughness: 1,
220
+ emissiveIntensity: 0,
221
+ wireframe: true,
222
+ side: three.DoubleSide
223
+ }
224
+ }
225
+ };
226
+ /** A look's preset, with its `lowPower` fields applied on a low-power device. */
227
+ function lookPreset(look, lowPower = false) {
228
+ const preset = LOOK_PRESETS[look];
229
+ return lowPower ? {
230
+ ...preset,
231
+ ...preset.lowPower
232
+ } : preset;
233
+ }
234
+ /** `userData` key marking a material whose emission came from the wire, not from a selection tint. */
235
+ const OWN_EMISSIVE = "selvaEmissive";
236
+ /** `userData` key holding the light a material casts, from real lumens rather than its look. */
237
+ const EMISSIVE_LIGHT = "selvaEmissiveLight";
238
+ /**
239
+ * `userData` key holding a material's own `envMapIntensity`, set by the parser when the wire
240
+ * material carries one. `setLook` keeps it instead of the look's value, except in looks with a
241
+ * `materialOverride`: those restyle every mesh alike, so the look's value wins there.
242
+ */
243
+ const OWN_ENV_MAP_INTENSITY = "selvaEnvMapIntensity";
244
+ /**
245
+ * A look dims `envMapIntensity` to turn down fill, but three scales a material's diffuse and
246
+ * specular IBL together. A metal has no diffuse, so the same dimming would only take away its
247
+ * reflection, which is all a metal shows. Blending toward 1 by metalness keeps a dielectric's fill
248
+ * on the look and a metal's reflection the same in every look.
249
+ */
250
+ function lookEnvMapIntensity(lookValue, metalness) {
251
+ return lookValue + (1 - lookValue) * Math.min(1, Math.max(0, metalness));
252
+ }
253
+ /** Baked at parse time (not toggleable at runtime). */
254
+ function materialAppearanceForLook(look) {
255
+ const preset = LOOK_PRESETS[look];
256
+ return {
257
+ envMapIntensity: preset.envMapIntensity,
258
+ cullBackfaces: preset.cullBackfaces
259
+ };
260
+ }
261
+ // src/shared/geometry.ts
262
+ function parseColor(colorString) {
263
+ if (!colorString || typeof colorString !== "string") {
264
+ getLogger().warn(`Invalid color input: ${colorString}, using white`);
265
+ return new three.Color(16777215);
266
+ }
267
+ const trimmed = colorString.trim();
268
+ if (/^#?[0-9A-Fa-f]{6}$/.test(trimmed)) try {
269
+ const hex = trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
270
+ return new three.Color(hex);
271
+ } catch {
272
+ getLogger().warn(`Invalid hex color: ${colorString}, using white`);
273
+ return new three.Color(16777215);
274
+ }
275
+ if (trimmed.includes(",")) {
276
+ const rgb = trimmed.split(",").map((c) => parseInt(c.trim(), 10));
277
+ if (rgb.length === 3 && rgb.every((n) => !isNaN(n) && n >= 0 && n <= 255)) return new three.Color(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255);
278
+ }
279
+ const named = trimmed.toLowerCase();
280
+ if (named in three.Color.NAMES) return new three.Color(three.Color.NAMES[named]);
281
+ getLogger().warn(`Invalid color string: ${colorString}, using white`);
282
+ return new three.Color(16777215);
283
+ }
284
+ /** Defaults to `z` (Rhino's up axis); pass an explicit axis when the scene uses a different `sceneUp`. */
285
+ function applyOffset(meshes, offset, axis = "z") {
286
+ meshes.forEach((mesh) => {
287
+ mesh.position[axis] -= offset;
288
+ });
289
+ }
290
+ function computeCombinedBoundingBox(meshes) {
291
+ const combinedBoundingBox = new three.Box3();
292
+ if (meshes.length === 0) return combinedBoundingBox;
293
+ meshes.forEach((mesh) => {
294
+ mesh.updateMatrixWorld(true);
295
+ const bbox = new three.Box3().setFromObject(mesh);
296
+ combinedBoundingBox.union(bbox);
297
+ });
298
+ return combinedBoundingBox;
299
+ }
300
+ // src/shared/gpu-capabilities.ts
301
+ const observers = /* @__PURE__ */ new Set();
302
+ let maxAnisotropy = 1;
303
+ function publishMaxAnisotropy(value) {
304
+ const next = Math.max(1, value);
305
+ if (next === maxAnisotropy) return;
306
+ maxAnisotropy = next;
307
+ for (const observe of observers) observe(next);
308
+ }
309
+ /** Fires immediately with the current value (1 until a renderer has reported), then on every change. */
310
+ function observeMaxAnisotropy(observe) {
311
+ observers.add(observe);
312
+ observe(maxAnisotropy);
313
+ return () => observers.delete(observe);
314
+ }
315
+ // src/shared/gpu-dispose.ts
316
+ /** Frees a material and its textures, respecting ownership (`./gpu-ownership.ts`). */
317
+ function disposeMaterial(material) {
318
+ if (!require_gpu_ownership.canDisposeMaterial(material)) return;
319
+ for (const [key, value] of Object.entries(material)) {
320
+ if (key === "envMap") continue;
321
+ if (value instanceof three.Texture) value.dispose();
322
+ }
323
+ material.dispose();
324
+ }
325
+ function disposeMaterials(material) {
326
+ if (!material) return;
327
+ if (Array.isArray(material)) material.forEach(disposeMaterial);
328
+ else disposeMaterial(material);
329
+ }
330
+ /**
331
+ * Frees the GPU resources of an object subtree, respecting ownership. **The only traversal that
332
+ * should dispose scene content** — a second disposing `traverse` risks missing an ownership guard.
333
+ */
334
+ function disposeObjectTree(root, options = {}) {
335
+ const { materials = true, keepUploaded } = options;
336
+ root.traverse((object) => {
337
+ const renderable = object;
338
+ if (!renderable.geometry && !renderable.material) return;
339
+ if (renderable.geometry) disposeGeometry(renderable.geometry, keepUploaded);
340
+ if (materials) disposeMaterials(renderable.material);
341
+ });
342
+ }
343
+ function disposeGeometry(geometry, keepUploaded) {
344
+ const source = require_gpu_ownership.cachedSourceOf(geometry);
345
+ if (source !== geometry) {
346
+ if (source) {
347
+ geometry.setIndex(null);
348
+ for (const name of Object.keys(geometry.attributes)) geometry.deleteAttribute(name);
349
+ }
350
+ geometry.dispose();
351
+ }
352
+ if (source && keepUploaded && !keepUploaded.has(source)) source.dispose();
353
+ }
354
+ Object.defineProperty(exports, "DEFAULT_LOOK", {
355
+ enumerable: true,
356
+ get: function() {
357
+ return DEFAULT_LOOK;
358
+ }
359
+ });
360
+ Object.defineProperty(exports, "EMISSIVE_LIGHT", {
361
+ enumerable: true,
362
+ get: function() {
363
+ return EMISSIVE_LIGHT;
364
+ }
365
+ });
366
+ Object.defineProperty(exports, "ErrorCodes", {
367
+ enumerable: true,
368
+ get: function() {
369
+ return ErrorCodes;
370
+ }
371
+ });
372
+ Object.defineProperty(exports, "LOOK_PRESETS", {
373
+ enumerable: true,
374
+ get: function() {
375
+ return LOOK_PRESETS;
376
+ }
377
+ });
378
+ Object.defineProperty(exports, "OWN_EMISSIVE", {
379
+ enumerable: true,
380
+ get: function() {
381
+ return OWN_EMISSIVE;
382
+ }
383
+ });
384
+ Object.defineProperty(exports, "OWN_ENV_MAP_INTENSITY", {
385
+ enumerable: true,
386
+ get: function() {
387
+ return OWN_ENV_MAP_INTENSITY;
388
+ }
389
+ });
390
+ Object.defineProperty(exports, "VisualizationError", {
391
+ enumerable: true,
392
+ get: function() {
393
+ return VisualizationError;
394
+ }
395
+ });
396
+ Object.defineProperty(exports, "applyOffset", {
397
+ enumerable: true,
398
+ get: function() {
399
+ return applyOffset;
400
+ }
401
+ });
402
+ Object.defineProperty(exports, "computeCombinedBoundingBox", {
403
+ enumerable: true,
404
+ get: function() {
405
+ return computeCombinedBoundingBox;
406
+ }
407
+ });
408
+ Object.defineProperty(exports, "disposeObjectTree", {
409
+ enumerable: true,
410
+ get: function() {
411
+ return disposeObjectTree;
412
+ }
413
+ });
414
+ Object.defineProperty(exports, "enableDebugLogging", {
415
+ enumerable: true,
416
+ get: function() {
417
+ return enableDebugLogging;
418
+ }
419
+ });
420
+ Object.defineProperty(exports, "getLogger", {
421
+ enumerable: true,
422
+ get: function() {
423
+ return getLogger;
424
+ }
425
+ });
426
+ Object.defineProperty(exports, "lookEnvMapIntensity", {
427
+ enumerable: true,
428
+ get: function() {
429
+ return lookEnvMapIntensity;
430
+ }
431
+ });
432
+ Object.defineProperty(exports, "lookPreset", {
433
+ enumerable: true,
434
+ get: function() {
435
+ return lookPreset;
436
+ }
437
+ });
438
+ Object.defineProperty(exports, "materialAppearanceForLook", {
439
+ enumerable: true,
440
+ get: function() {
441
+ return materialAppearanceForLook;
442
+ }
443
+ });
444
+ Object.defineProperty(exports, "observeMaxAnisotropy", {
445
+ enumerable: true,
446
+ get: function() {
447
+ return observeMaxAnisotropy;
448
+ }
449
+ });
450
+ Object.defineProperty(exports, "parseColor", {
451
+ enumerable: true,
452
+ get: function() {
453
+ return parseColor;
454
+ }
455
+ });
456
+ Object.defineProperty(exports, "publishMaxAnisotropy", {
457
+ enumerable: true,
458
+ get: function() {
459
+ return publishMaxAnisotropy;
460
+ }
461
+ });
462
+ Object.defineProperty(exports, "setLogger", {
463
+ enumerable: true,
464
+ get: function() {
465
+ return setLogger;
466
+ }
467
+ });
468
+
469
+ //# sourceMappingURL=gpu-dispose-Cxu3dFmA.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gpu-dispose-Cxu3dFmA.cjs","names":["THREE","THREE","canDisposeMaterial","THREE","cachedSourceOf"],"sources":["../src/shared/errors.ts","../src/shared/logger.ts","../src/shared/looks.ts","../src/shared/geometry.ts","../src/shared/gpu-capabilities.ts","../src/shared/gpu-dispose.ts"],"sourcesContent":["/**\n * Errors for the visualization package. Replaces `@selvajs/compute`'s `RhinoComputeError`, which\n * mis-named failures on paths (e.g. the plugin WebSocket) that never touch Rhino.Compute. `code`\n * values match compute's so existing catch-sites keep working.\n */\n\nexport const ErrorCodes = {\n\t/** Structural check failed: bad magic bytes, out-of-window index, malformed metadata. */\n\tVALIDATION_ERROR: 'VALIDATION_ERROR',\n\tINVALID_STATE: 'INVALID_STATE',\n\t/** No `DecompressionStream`, no WebGL context, etc. */\n\tENVIRONMENT_ERROR: 'ENVIRONMENT_ERROR',\n\tINVALID_CONFIG: 'INVALID_CONFIG',\n\t/** Base64 input could not be decoded. */\n\tENCODING_ERROR: 'ENCODING_ERROR',\n\tUNKNOWN_ERROR: 'UNKNOWN_ERROR'\n} as const;\n\nexport type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];\n\nexport class VisualizationError extends Error {\n\tpublic readonly code: ErrorCode;\n\tpublic readonly context?: Record<string, unknown>;\n\tpublic readonly originalError?: Error;\n\n\tconstructor(\n\t\tmessage: string,\n\t\tcode: ErrorCode = ErrorCodes.UNKNOWN_ERROR,\n\t\toptions?: { context?: Record<string, unknown>; originalError?: Error }\n\t) {\n\t\tsuper(message);\n\t\tthis.name = 'VisualizationError';\n\t\tthis.code = code;\n\t\tthis.context = options?.context;\n\t\tthis.originalError = options?.originalError;\n\t\tif (options?.originalError) {\n\t\t\t(this as { cause?: unknown }).cause = options.originalError;\n\t\t}\n\t}\n}\n","/**\n * Logging facility for the visualization package. Deliberately local rather than imported from\n * `@selvajs/compute` (logging isn't a compute concern). Mirrors compute's logger shape so a host\n * wanting one sink for both can call `setLogger(computeLogger.getLogger())`.\n */\n\nimport { VisualizationError, ErrorCodes } from './errors.js';\n\nexport interface Logger {\n\tdebug(message: string, ...args: unknown[]): void;\n\tinfo(message: string, ...args: unknown[]): void;\n\twarn(message: string, ...args: unknown[]): void;\n\terror(message: string, ...args: unknown[]): void;\n}\n\nclass NoOpLogger implements Logger {\n\tdebug(): void {}\n\tinfo(): void {}\n\twarn(): void {}\n\terror(): void {}\n}\n\nclass ConsoleLogger implements Logger {\n\tdebug(message: string, ...args: unknown[]): void {\n\t\t// eslint-disable-next-line no-console -- this class exists to be the console sink\n\t\tconsole.debug(message, ...args);\n\t}\n\n\tinfo(message: string, ...args: unknown[]): void {\n\t\tconsole.info(message, ...args);\n\t}\n\n\twarn(message: string, ...args: unknown[]): void {\n\t\tconsole.warn(message, ...args);\n\t}\n\n\terror(message: string, ...args: unknown[]): void {\n\t\tconsole.error(message, ...args);\n\t}\n}\n\n// Defaults to no-op: a library that writes to the console uninvited is a nuisance in a host with\n// its own logging. Opt in with setLogger or enableDebugLogging.\nlet internalLogger: Logger = new NoOpLogger();\n\n// Call per-use rather than caching — the sink is settable at any time.\nexport function getLogger(): Logger {\n\treturn internalLogger;\n}\n\n// Validates logger shape up front: failing here beats a confusing \"getLogger().debug is not a\n// function\" at some later, unrelated call site.\nexport function setLogger(logger: Logger | Console | null): void {\n\tif (logger === null) {\n\t\tinternalLogger = new NoOpLogger();\n\t\treturn;\n\t}\n\n\tconst missing = (['debug', 'info', 'warn', 'error'] as const).filter(\n\t\t(method) => typeof (logger as unknown as Record<string, unknown>)[method] !== 'function'\n\t);\n\tif (missing.length > 0) {\n\t\tthrow new VisualizationError(\n\t\t\t`Logger is missing required method(s): ${missing.join(', ')}. A logger must implement debug, info, warn and error.`,\n\t\t\tErrorCodes.INVALID_CONFIG,\n\t\t\t{ context: { missingMethods: missing } }\n\t\t);\n\t}\n\n\tinternalLogger = logger as Logger;\n}\n\nexport function enableDebugLogging(): void {\n\tsetLogger(new ConsoleLogger());\n}\n","import * as THREE from 'three';\n\nimport type { Look, LookPreset, MaterialAppearanceOptions } from './types.js';\n\n/** The look applied when the caller passes no `look` option. */\nexport const DEFAULT_LOOK: Look = 'technical';\n\n/**\n * Single source of truth for both `applyDefaults` (construction) and `setLook` (runtime), so the two\n * can't drift.\n *\n * Every shaded look pairs a key light with contact shading (GTAO), because image-based lighting on\n * its own lights all faces of a box nearly equally — the result is a white silhouette with no\n * readable form. GTAO is a full-screen pass and does cost frames; `xray` skips it since there are no\n * opaque contacts left to shade.\n */\nexport const LOOK_PRESETS: Record<Look, LookPreset> = {\n\t// Fills shadows in (vs. showcase, which lets them fall off), but still key-lit: ACES plus a\n\t// moderate sun, with fill high enough that shadowed faces keep detail.\n\tstudio: {\n\t\ttoneMapping: THREE.ACESFilmicToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\tenvMapIntensity: 1.0,\n\t\tenvironmentIntensity: 0.85,\n\t\themisphereIntensity: 0.5,\n\t\tambientIntensity: 0.25,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 1.6\n\t},\n\t// The sun carries this look and the ambient/IBL only lift shadows off black. Fill was the thing\n\t// making it read as flat white paper: every source except the sun is orientation-independent, so\n\t// the more of them you stack the less the three faces of a corner differ.\n\ttechnical: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\tenvMapIntensity: 0.55,\n\t\tenvironmentIntensity: 0.6,\n\t\themisphereIntensity: 0.25,\n\t\tambientIntensity: 0.15,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 2.2\n\t},\n\t// Fill pulled furthest DOWN and the sun pushed hardest, so light-to-shadow falloff is steepest —\n\t// a dramatic hero shot, unlike `studio` which fills shadows back in.\n\tshowcase: {\n\t\ttoneMapping: THREE.ACESFilmicToneMapping,\n\t\ttoneMappingExposure: 1.15,\n\t\tenvMapIntensity: 1.4,\n\t\tenvironmentIntensity: 1.0,\n\t\themisphereIntensity: 0.2,\n\t\tambientIntensity: 0.1,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 2.6\n\t},\n\t// Every mesh forced to one near-white clay, so form and shadow carry the image instead of the\n\t// model's own colours. AO on: with all albedo differences gone, contact shading is the only thing\n\t// left separating touching parts — without it the model reads as one white blob.\n\tarctic: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1.05,\n\t\tenvMapIntensity: 0.85,\n\t\tenvironmentIntensity: 1.1,\n\t\themisphereIntensity: 0.3,\n\t\tambientIntensity: 0.15,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 2.0,\n\t\tmaterialOverride: {\n\t\t\tcolor: 0xeef1f5,\n\t\t\tmetalness: 0,\n\t\t\troughness: 0.75,\n\t\t\temissiveIntensity: 0\n\t\t}\n\t},\n\t// See-through blue-white with depth writes off, so interior geometry shows through the shell.\n\t// Exposure pushed up because stacked transparent layers darken fast; the sun is dialled back\n\t// because a hard highlight on a transparent shell hides what is behind it.\n\txray: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1.3,\n\t\tenvMapIntensity: 0.4,\n\t\tenvironmentIntensity: 0.8,\n\t\themisphereIntensity: 0.6,\n\t\tambientIntensity: 0.6,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: false,\n\t\tsunlightIntensity: 0.6,\n\t\tmaterialOverride: {\n\t\t\tcolor: 0x9fc4e8,\n\t\t\tmetalness: 0,\n\t\t\troughness: 0.9,\n\t\t\temissiveIntensity: 0,\n\t\t\topacity: 0.28,\n\t\t\tdepthWrite: false,\n\t\t\tside: THREE.DoubleSide\n\t\t}\n\t},\n\t// The architectural line drawing: feature edges over flat white faces. The faces are the whole\n\t// point even though you never really see them — they still write depth, so a near member hides\n\t// the ones behind it. True wireframe can't do that, and on a dense frame (thousands of members,\n\t// every edge of every one drawn at once) it collapses into a black mass.\n\t//\n\t// Unlit on purpose: any directional shading would compete with the lines for the eye. Ambient\n\t// alone lands every face on the same flat white, so the only contrast in the image is the edges.\n\t// AO is off for the same reason — a grey contact smear under a line drawing just muddies it.\n\tlineart: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\t// The faces are MeshPhysicalMaterial, so they take their brightness from lighting, not from\n\t\t// `color`: zero out the lights and they render black however white the override is. Hemisphere\n\t\t// and ambient carry it rather than IBL, because `scene.environment` only exists if the host\n\t\t// loaded an HDR — leaning on IBL would render this look black in any viewer without one. Both\n\t\t// are orientation-independent, so every face lands on the same flat white and the edges stay\n\t\t// the only contrast in the image. Ambient is weighted over hemisphere because hemisphere's\n\t\t// ground colour is a warm brown by default: leaning on it tints every downward face grey and\n\t\t// the drawing stops reading as one flat white.\n\t\tenvMapIntensity: 1.0,\n\t\tenvironmentIntensity: 1.0,\n\t\themisphereIntensity: 0.6,\n\t\tambientIntensity: 3.2,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: false,\n\t\tsunlightIntensity: 0,\n\t\trequiresEdges: true,\n\t\tmaterialOverride: {\n\t\t\tcolor: 0xf7f8fa,\n\t\t\tmetalness: 0,\n\t\t\troughness: 1,\n\t\t\temissiveIntensity: 0\n\t\t}\n\t},\n\t// Triangle edges only — the tessellation itself, diagonals and all. That makes it a mesh-\n\t// inspection tool, not a drawing: for a readable line drawing use `lineart`, which draws\n\t// feature edges and lets near faces occlude far ones. Nothing here is a lit surface:\n\t// the sun is off and fill carries the image, because a directional light on a line renders it\n\t// black wherever the line runs away from the key. AO is off for the same reason — a full-screen\n\t// contact pass has no contacts to find between wires, and it only costs frames.\n\t// After dark: the only real light comes from what the model emits, so a lamp lights its desk.\n\t// A little sky stays so the unlit half of the model keeps its shape instead of going black.\n\tnight: {\n\t\ttoneMapping: THREE.ACESFilmicToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\tenvMapIntensity: 0.32,\n\t\tenvironmentIntensity: 0.15,\n\t\themisphereIntensity: 0.26,\n\t\tambientIntensity: 0.05,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: true,\n\t\tsunlightIntensity: 0.14,\n\t\tbackground: 0x0a0e12,\n\t\t// Above what the environment reaches on polished metal, so only emitters glow.\n\t\tbloom: { strength: 0.2, radius: 0.3, threshold: 1.8 },\n\t\temissiveLights: true,\n\t\t// Full-screen passes are the cost here: AO alone takes a quarter of the frame on an integrated\n\t\t// GPU, and without it there is no composer. A bounce light costs next to nothing and is all\n\t\t// that lights the room outside the shade, so one stays with its key.\n\t\tlowPower: {\n\t\t\tambientOcclusion: false,\n\t\t\tbloom: undefined,\n\t\t\tmaxEmissiveLights: 1,\n\t\t\tmaxPixelRatio: 1.5\n\t\t}\n\t},\n\twireframe: {\n\t\ttoneMapping: THREE.NeutralToneMapping,\n\t\ttoneMappingExposure: 1,\n\t\t// Lit for the same reason as lineart (see there): PBR lines are black unlit, and hemisphere\n\t\t// plus ambient work with no HDR loaded. Dimmer than lineart because these lines are dark\n\t\t// on white, not white on dark.\n\t\tenvMapIntensity: 1.0,\n\t\tenvironmentIntensity: 1.0,\n\t\themisphereIntensity: 0.4,\n\t\tambientIntensity: 2.0,\n\t\tcullBackfaces: false,\n\t\tambientOcclusion: false,\n\t\tsunlightIntensity: 0,\n\t\tmaterialOverride: {\n\t\t\tcolor: 0x2b3138,\n\t\t\tmetalness: 0,\n\t\t\troughness: 1,\n\t\t\temissiveIntensity: 0,\n\t\t\twireframe: true,\n\t\t\tside: THREE.DoubleSide\n\t\t}\n\t}\n};\n\n/** A look's preset, with its `lowPower` fields applied on a low-power device. */\nexport function lookPreset(look: Look, lowPower = false): LookPreset {\n\tconst preset = LOOK_PRESETS[look];\n\treturn lowPower ? { ...preset, ...preset.lowPower } : preset;\n}\n\n/** `userData` key marking a material whose emission came from the wire, not from a selection tint. */\nexport const OWN_EMISSIVE = 'selvaEmissive';\n\n/** `userData` key holding the light a material casts, from real lumens rather than its look. */\nexport const EMISSIVE_LIGHT = 'selvaEmissiveLight';\n\n/**\n * `userData` key holding a material's own `envMapIntensity`, set by the parser when the wire\n * material carries one. `setLook` keeps it instead of the look's value, except in looks with a\n * `materialOverride`: those restyle every mesh alike, so the look's value wins there.\n */\nexport const OWN_ENV_MAP_INTENSITY = 'selvaEnvMapIntensity';\n\n/**\n * A look dims `envMapIntensity` to turn down fill, but three scales a material's diffuse and\n * specular IBL together. A metal has no diffuse, so the same dimming would only take away its\n * reflection, which is all a metal shows. Blending toward 1 by metalness keeps a dielectric's fill\n * on the look and a metal's reflection the same in every look.\n */\nexport function lookEnvMapIntensity(lookValue: number, metalness: number): number {\n\treturn lookValue + (1 - lookValue) * Math.min(1, Math.max(0, metalness));\n}\n\n/** Baked at parse time (not toggleable at runtime). */\nexport function materialAppearanceForLook(look: Look): MaterialAppearanceOptions {\n\tconst preset = LOOK_PRESETS[look];\n\treturn {\n\t\tenvMapIntensity: preset.envMapIntensity,\n\t\tcullBackfaces: preset.cullBackfaces\n\t};\n}\n","import * as THREE from 'three';\n\nimport { getLogger } from './logger.js';\n\n// parse/ needs these and must never import upward from render/, so they live in shared/ instead\n// of render's three-helpers.\n\nexport function parseColor(colorString: string): THREE.Color {\n\tif (!colorString || typeof colorString !== 'string') {\n\t\tgetLogger().warn(`Invalid color input: ${colorString}, using white`);\n\t\treturn new THREE.Color(0xffffff);\n\t}\n\n\tconst trimmed = colorString.trim();\n\n\tif (/^#?[0-9A-Fa-f]{6}$/.test(trimmed)) {\n\t\ttry {\n\t\t\tconst hex = trimmed.startsWith('#') ? trimmed : `#${trimmed}`;\n\t\t\treturn new THREE.Color(hex);\n\t\t} catch {\n\t\t\tgetLogger().warn(`Invalid hex color: ${colorString}, using white`);\n\t\t\treturn new THREE.Color(0xffffff);\n\t\t}\n\t}\n\n\tif (trimmed.includes(',')) {\n\t\tconst rgb = trimmed.split(',').map((c) => parseInt(c.trim(), 10));\n\t\tif (rgb.length === 3 && rgb.every((n) => !isNaN(n) && n >= 0 && n <= 255)) {\n\t\t\treturn new THREE.Color(rgb[0] / 255, rgb[1] / 255, rgb[2] / 255);\n\t\t}\n\t}\n\n\t// `new THREE.Color(name)` never throws on unknown names (it logs its own warning and leaves the\n\t// color white), so validate against three's CSS name table instead.\n\tconst named = trimmed.toLowerCase();\n\tif (named in THREE.Color.NAMES) {\n\t\treturn new THREE.Color(THREE.Color.NAMES[named as keyof typeof THREE.Color.NAMES]);\n\t}\n\n\tgetLogger().warn(`Invalid color string: ${colorString}, using white`);\n\treturn new THREE.Color(0xffffff);\n}\n\n/** Defaults to `z` (Rhino's up axis); pass an explicit axis when the scene uses a different `sceneUp`. */\nexport function applyOffset(\n\tmeshes: THREE.Object3D[],\n\toffset: number,\n\taxis: 'x' | 'y' | 'z' = 'z'\n): void {\n\tmeshes.forEach((mesh) => {\n\t\tmesh.position[axis] -= offset;\n\t});\n}\n\nexport function computeCombinedBoundingBox(meshes: THREE.Object3D[]): THREE.Box3 {\n\tconst combinedBoundingBox = new THREE.Box3();\n\tif (meshes.length === 0) return combinedBoundingBox;\n\tmeshes.forEach((mesh) => {\n\t\tmesh.updateMatrixWorld(true);\n\t\tconst bbox = new THREE.Box3().setFromObject(mesh);\n\t\tcombinedBoundingBox.union(bbox);\n\t});\n\treturn combinedBoundingBox;\n}\n","// ============================================================================\n// GPU capabilities — published by the renderer, read by whoever needs them\n// ============================================================================\n\n// Lets a renderer publish GPU capabilities (max anisotropy) without `render/` and `parse/` importing\n// each other: the renderer publishes on init, interested parties subscribe.\n\ntype AnisotropyObserver = (value: number) => void;\n\nconst observers = new Set<AnisotropyObserver>();\n\n// Module load order between layers isn't guaranteed, so a subscriber arriving after init still\n// needs the current value rather than waiting for a second renderer to publish.\nlet maxAnisotropy = 1;\n\nexport function publishMaxAnisotropy(value: number): void {\n\tconst next = Math.max(1, value);\n\tif (next === maxAnisotropy) return;\n\tmaxAnisotropy = next;\n\tfor (const observe of observers) observe(next);\n}\n\n/** Fires immediately with the current value (1 until a renderer has reported), then on every change. */\nexport function observeMaxAnisotropy(observe: AnisotropyObserver): () => void {\n\tobservers.add(observe);\n\tobserve(maxAnisotropy);\n\treturn () => observers.delete(observe);\n}\n","import * as THREE from 'three';\n\nimport { cachedSourceOf, canDisposeMaterial } from './gpu-ownership.js';\n\n// ============================================================================\n// The disposal walkers — every teardown path in the package goes through these\n// ============================================================================\n\n/** Frees a material and its textures, respecting ownership (`./gpu-ownership.ts`). */\nexport function disposeMaterial(material: THREE.Material): void {\n\tif (!canDisposeMaterial(material)) return;\n\n\tfor (const [key, value] of Object.entries(material)) {\n\t\t// An envMap is the scene's, shared by every metal: freeing it here would blank them all.\n\t\tif (key === 'envMap') continue;\n\t\tif (value instanceof THREE.Texture) {\n\t\t\tvalue.dispose();\n\t\t}\n\t}\n\tmaterial.dispose();\n}\n\nfunction disposeMaterials(material: THREE.Material | THREE.Material[] | undefined): void {\n\tif (!material) return;\n\tif (Array.isArray(material)) material.forEach(disposeMaterial);\n\telse disposeMaterial(material);\n}\n\nexport interface DisposeOptions {\n\t/** Free materials and their textures too. Default true; the solve memo passes false. */\n\tmaterials?: boolean;\n\t/** Called for each geometry before disposal, so a subsystem can free derived resources keyed on it (e.g. edge caches). */\n\tonGeometry?: (geometry: THREE.BufferGeometry) => void;\n\t/**\n\t * Cached geometries whose GPU copy survives. When given, every other cached geometry this walk\n\t * reaches loses its GPU copy.\n\t */\n\tkeepUploaded?: ReadonlySet<THREE.BufferGeometry>;\n}\n\n/**\n * Frees the GPU resources of an object subtree, respecting ownership. **The only traversal that\n * should dispose scene content** — a second disposing `traverse` risks missing an ownership guard.\n */\nexport function disposeObjectTree(root: THREE.Object3D, options: DisposeOptions = {}): void {\n\tconst { materials = true, keepUploaded } = options;\n\n\troot.traverse((object) => {\n\t\tconst renderable = object as Partial<THREE.Mesh> & THREE.Object3D;\n\t\tif (!renderable.geometry && !renderable.material) return;\n\n\t\tif (renderable.geometry) disposeGeometry(renderable.geometry, keepUploaded);\n\n\t\tif (materials) disposeMaterials(renderable.material);\n\t});\n}\n\nfunction disposeGeometry(\n\tgeometry: THREE.BufferGeometry,\n\tkeepUploaded: ReadonlySet<THREE.BufferGeometry> | undefined\n): void {\n\tconst source = cachedSourceOf(geometry);\n\tif (source !== geometry) {\n\t\t// three's dispose frees the GPU buffers of whatever attributes are attached, so a borrower\n\t\t// lets go of the cache's attributes first.\n\t\tif (source) {\n\t\t\tgeometry.setIndex(null);\n\t\t\tfor (const name of Object.keys(geometry.attributes)) geometry.deleteAttribute(name);\n\t\t}\n\t\tgeometry.dispose();\n\t}\n\tif (source && keepUploaded && !keepUploaded.has(source)) source.dispose();\n}\n"],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ // \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ // src/shared/gpu-ownership.ts
23
+ /**
24
+ * A GPU resource has exactly one owner: a **module singleton** (nobody ever frees it), a **cache**
25
+ * (frees it on eviction) or the **scene** (freed on teardown). Every disposal path asks
26
+ * {@link canDisposeMaterial} before freeing a material. Textures are always scene-owned.
27
+ *
28
+ * A cached geometry's data belongs to the cache, but its GPU copy lives only while a scene draws it.
29
+ * Scene teardown resolves each geometry with {@link cachedSourceOf} and frees the GPU copy of every
30
+ * cached geometry the next scene does not draw; three uploads it again if it comes back. A cached
31
+ * geometry can back several meshes at once, so nothing may write to its arrays.
32
+ */
33
+ const protectedMaterials = /* @__PURE__ */ new Set();
34
+ /** True when `material` is scene-owned (not one of the protected singletons). */
35
+ function canDisposeMaterial(material) {
36
+ if (!material) return false;
37
+ return !protectedMaterials.has(material);
38
+ }
39
+ const sharedGeometries = /* @__PURE__ */ new WeakSet();
40
+ function shareGeometry(geometry) {
41
+ sharedGeometries.add(geometry);
42
+ }
43
+ function unshareGeometry(geometry) {
44
+ sharedGeometries.delete(geometry);
45
+ }
46
+ function isCachedGeometry(geometry) {
47
+ return sharedGeometries.has(geometry);
48
+ }
49
+ const borrowers = /* @__PURE__ */ new WeakMap();
50
+ function borrowGeometry(borrower, source) {
51
+ borrowers.set(borrower, source);
52
+ }
53
+ /** The cached geometry whose GPU buffers `geometry` draws, or null when it owns its own. */
54
+ function cachedSourceOf(geometry) {
55
+ if (sharedGeometries.has(geometry)) return geometry;
56
+ const source = borrowers.get(geometry);
57
+ return source && sharedGeometries.has(source) ? source : null;
58
+ }
59
+ Object.defineProperty(exports, "__toESM", {
60
+ enumerable: true,
61
+ get: function() {
62
+ return __toESM;
63
+ }
64
+ });
65
+ Object.defineProperty(exports, "borrowGeometry", {
66
+ enumerable: true,
67
+ get: function() {
68
+ return borrowGeometry;
69
+ }
70
+ });
71
+ Object.defineProperty(exports, "cachedSourceOf", {
72
+ enumerable: true,
73
+ get: function() {
74
+ return cachedSourceOf;
75
+ }
76
+ });
77
+ Object.defineProperty(exports, "canDisposeMaterial", {
78
+ enumerable: true,
79
+ get: function() {
80
+ return canDisposeMaterial;
81
+ }
82
+ });
83
+ Object.defineProperty(exports, "isCachedGeometry", {
84
+ enumerable: true,
85
+ get: function() {
86
+ return isCachedGeometry;
87
+ }
88
+ });
89
+ Object.defineProperty(exports, "shareGeometry", {
90
+ enumerable: true,
91
+ get: function() {
92
+ return shareGeometry;
93
+ }
94
+ });
95
+ Object.defineProperty(exports, "unshareGeometry", {
96
+ enumerable: true,
97
+ get: function() {
98
+ return unshareGeometry;
99
+ }
100
+ });
101
+
102
+ //# sourceMappingURL=gpu-ownership-ADRZBxxs.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gpu-ownership-ADRZBxxs.cjs","names":[],"sources":["../src/shared/gpu-ownership.ts"],"sourcesContent":["import type * as THREE from 'three';\n\n// ============================================================================\n// GPU resource ownership — the single rule every disposal path obeys\n// ============================================================================\n\n/**\n * A GPU resource has exactly one owner: a **module singleton** (nobody ever frees it), a **cache**\n * (frees it on eviction) or the **scene** (freed on teardown). Every disposal path asks\n * {@link canDisposeMaterial} before freeing a material. Textures are always scene-owned.\n *\n * A cached geometry's data belongs to the cache, but its GPU copy lives only while a scene draws it.\n * Scene teardown resolves each geometry with {@link cachedSourceOf} and frees the GPU copy of every\n * cached geometry the next scene does not draw; three uploads it again if it comes back. A cached\n * geometry can back several meshes at once, so nothing may write to its arrays.\n */\n\n// Module-scope singletons from `render/three-materials.ts`, shared across meshes and solves —\n// disposing one would free textures still in use and force a recompile. Held here (not imported\n// there) to avoid an import cycle.\nconst protectedMaterials = new Set<THREE.Material>();\n\n/** Register materials as never-disposable. Called once by `render/three-materials.ts`. */\nexport function protectMaterials(materials: Iterable<THREE.Material>): void {\n\tfor (const material of materials) protectedMaterials.add(material);\n}\n\n/** True when `material` is scene-owned (not one of the protected singletons). */\nexport function canDisposeMaterial(material: THREE.Material | undefined): boolean {\n\tif (!material) return false;\n\treturn !protectedMaterials.has(material);\n}\n\n// Geometries the parser's finished-geometry cache hands out across solves.\nconst sharedGeometries = new WeakSet<THREE.BufferGeometry>();\n\nexport function shareGeometry(geometry: THREE.BufferGeometry): void {\n\tsharedGeometries.add(geometry);\n}\n\nexport function unshareGeometry(geometry: THREE.BufferGeometry): void {\n\tsharedGeometries.delete(geometry);\n}\n\nexport function isCachedGeometry(geometry: THREE.BufferGeometry): boolean {\n\treturn sharedGeometries.has(geometry);\n}\n\n// Scene-owned geometries drawing a cached geometry's attributes under their own groups. Disposing\n// one as is would free those attributes' GPU buffers out from under the cache.\nconst borrowers = new WeakMap<THREE.BufferGeometry, THREE.BufferGeometry>();\n\nexport function borrowGeometry(borrower: THREE.BufferGeometry, source: THREE.BufferGeometry): void {\n\tborrowers.set(borrower, source);\n}\n\n/** The cached geometry whose GPU buffers `geometry` draws, or null when it owns its own. */\nexport function cachedSourceOf(geometry: THREE.BufferGeometry): THREE.BufferGeometry | null {\n\tif (sharedGeometries.has(geometry)) return geometry;\n\t// Once evicted, the source is scene-owned like any other geometry, and the borrower frees it.\n\tconst source = borrowers.get(geometry);\n\treturn source && sharedGeometries.has(source) ? source : null;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoBA,MAAM,qCAAqB,IAAI,IAAoB;;AAQnD,SAAgB,mBAAmB,UAA+C;CACjF,IAAI,CAAC,UAAU,OAAO;CACtB,OAAO,CAAC,mBAAmB,IAAI,QAAQ;AACxC;AAGA,MAAM,mCAAmB,IAAI,QAA8B;AAE3D,SAAgB,cAAc,UAAsC;CACnE,iBAAiB,IAAI,QAAQ;AAC9B;AAEA,SAAgB,gBAAgB,UAAsC;CACrE,iBAAiB,OAAO,QAAQ;AACjC;AAEA,SAAgB,iBAAiB,UAAyC;CACzE,OAAO,iBAAiB,IAAI,QAAQ;AACrC;AAIA,MAAM,4BAAY,IAAI,QAAoD;AAE1E,SAAgB,eAAe,UAAgC,QAAoC;CAClG,UAAU,IAAI,UAAU,MAAM;AAC/B;;AAGA,SAAgB,eAAe,UAA6D;CAC3F,IAAI,iBAAiB,IAAI,QAAQ,GAAG,OAAO;CAE3C,MAAM,SAAS,UAAU,IAAI,QAAQ;CACrC,OAAO,UAAU,iBAAiB,IAAI,MAAM,IAAI,SAAS;AAC1D"}
@@ -0,0 +1,40 @@
1
+ // src/shared/gpu-ownership.ts
2
+ /**
3
+ * A GPU resource has exactly one owner: a **module singleton** (nobody ever frees it), a **cache**
4
+ * (frees it on eviction) or the **scene** (freed on teardown). Every disposal path asks
5
+ * {@link canDisposeMaterial} before freeing a material. Textures are always scene-owned.
6
+ *
7
+ * A cached geometry's data belongs to the cache, but its GPU copy lives only while a scene draws it.
8
+ * Scene teardown resolves each geometry with {@link cachedSourceOf} and frees the GPU copy of every
9
+ * cached geometry the next scene does not draw; three uploads it again if it comes back. A cached
10
+ * geometry can back several meshes at once, so nothing may write to its arrays.
11
+ */
12
+ const protectedMaterials = /* @__PURE__ */ new Set();
13
+ /** True when `material` is scene-owned (not one of the protected singletons). */
14
+ function canDisposeMaterial(material) {
15
+ if (!material) return false;
16
+ return !protectedMaterials.has(material);
17
+ }
18
+ const sharedGeometries = /* @__PURE__ */ new WeakSet();
19
+ function shareGeometry(geometry) {
20
+ sharedGeometries.add(geometry);
21
+ }
22
+ function unshareGeometry(geometry) {
23
+ sharedGeometries.delete(geometry);
24
+ }
25
+ function isCachedGeometry(geometry) {
26
+ return sharedGeometries.has(geometry);
27
+ }
28
+ const borrowers = /* @__PURE__ */ new WeakMap();
29
+ function borrowGeometry(borrower, source) {
30
+ borrowers.set(borrower, source);
31
+ }
32
+ /** The cached geometry whose GPU buffers `geometry` draws, or null when it owns its own. */
33
+ function cachedSourceOf(geometry) {
34
+ if (sharedGeometries.has(geometry)) return geometry;
35
+ const source = borrowers.get(geometry);
36
+ return source && sharedGeometries.has(source) ? source : null;
37
+ }
38
+ export { shareGeometry as a, isCachedGeometry as i, cachedSourceOf as n, unshareGeometry as o, canDisposeMaterial as r, borrowGeometry as t };
39
+
40
+ //# sourceMappingURL=gpu-ownership-Bs9l7hrU.js.map