@appinternalleads/ui 0.4.1 → 0.4.3

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 (76) hide show
  1. package/README.md +108 -0
  2. package/dist/cjs/motion/player.js +3 -0
  3. package/dist/cjs/motion/player.js.map +1 -1
  4. package/dist/cjs/motion/typesurface/TypeSurfaceCanvas.js +25 -2
  5. package/dist/cjs/motion/typesurface/TypeSurfaceCanvas.js.map +1 -1
  6. package/dist/cjs/motion/typesurface/config.js +21 -1
  7. package/dist/cjs/motion/typesurface/config.js.map +1 -1
  8. package/dist/cjs/motion/typesurface/construct.js +875 -0
  9. package/dist/cjs/motion/typesurface/construct.js.map +1 -0
  10. package/dist/cjs/motion/typesurface/engine.js +28 -4
  11. package/dist/cjs/motion/typesurface/engine.js.map +1 -1
  12. package/dist/cjs/motion/typesurface/index.js +9 -1
  13. package/dist/cjs/motion/typesurface/index.js.map +1 -1
  14. package/dist/cjs/motion/typesurface/presets.js +69 -2
  15. package/dist/cjs/motion/typesurface/presets.js.map +1 -1
  16. package/dist/cjs/motion/typesurface/renderer.js +81 -15
  17. package/dist/cjs/motion/typesurface/renderer.js.map +1 -1
  18. package/dist/cjs/motion/typesurface/shaders.js +23 -1
  19. package/dist/cjs/motion/typesurface/shaders.js.map +1 -1
  20. package/dist/cjs/motion/typesurface/surfaces.js +69 -0
  21. package/dist/cjs/motion/typesurface/surfaces.js.map +1 -1
  22. package/dist/esm/motion/player.d.ts.map +1 -1
  23. package/dist/esm/motion/player.js +3 -0
  24. package/dist/esm/motion/player.js.map +1 -1
  25. package/dist/esm/motion/typesurface/TypeSurfaceCanvas.d.ts +11 -1
  26. package/dist/esm/motion/typesurface/TypeSurfaceCanvas.d.ts.map +1 -1
  27. package/dist/esm/motion/typesurface/TypeSurfaceCanvas.js +25 -2
  28. package/dist/esm/motion/typesurface/TypeSurfaceCanvas.js.map +1 -1
  29. package/dist/esm/motion/typesurface/config.d.ts +13 -2
  30. package/dist/esm/motion/typesurface/config.d.ts.map +1 -1
  31. package/dist/esm/motion/typesurface/config.js +21 -1
  32. package/dist/esm/motion/typesurface/config.js.map +1 -1
  33. package/dist/esm/motion/typesurface/construct.d.ts +284 -0
  34. package/dist/esm/motion/typesurface/construct.d.ts.map +1 -0
  35. package/dist/esm/motion/typesurface/construct.js +866 -0
  36. package/dist/esm/motion/typesurface/construct.js.map +1 -0
  37. package/dist/esm/motion/typesurface/engine.d.ts +8 -0
  38. package/dist/esm/motion/typesurface/engine.d.ts.map +1 -1
  39. package/dist/esm/motion/typesurface/engine.js +28 -4
  40. package/dist/esm/motion/typesurface/engine.js.map +1 -1
  41. package/dist/esm/motion/typesurface/index.d.ts +7 -3
  42. package/dist/esm/motion/typesurface/index.d.ts.map +1 -1
  43. package/dist/esm/motion/typesurface/index.js +6 -2
  44. package/dist/esm/motion/typesurface/index.js.map +1 -1
  45. package/dist/esm/motion/typesurface/presets.d.ts +18 -0
  46. package/dist/esm/motion/typesurface/presets.d.ts.map +1 -1
  47. package/dist/esm/motion/typesurface/presets.js +68 -3
  48. package/dist/esm/motion/typesurface/presets.js.map +1 -1
  49. package/dist/esm/motion/typesurface/renderer.d.ts +10 -0
  50. package/dist/esm/motion/typesurface/renderer.d.ts.map +1 -1
  51. package/dist/esm/motion/typesurface/renderer.js +82 -16
  52. package/dist/esm/motion/typesurface/renderer.js.map +1 -1
  53. package/dist/esm/motion/typesurface/shaders.d.ts +1 -1
  54. package/dist/esm/motion/typesurface/shaders.d.ts.map +1 -1
  55. package/dist/esm/motion/typesurface/shaders.js +23 -1
  56. package/dist/esm/motion/typesurface/shaders.js.map +1 -1
  57. package/dist/esm/motion/typesurface/surfaces.d.ts +28 -1
  58. package/dist/esm/motion/typesurface/surfaces.d.ts.map +1 -1
  59. package/dist/esm/motion/typesurface/surfaces.js +67 -0
  60. package/dist/esm/motion/typesurface/surfaces.js.map +1 -1
  61. package/docs/TYPE-SURFACE.md +188 -0
  62. package/package.json +4 -2
  63. package/src/motion/player.ts +3 -0
  64. package/src/motion/typesurface/TypeSurfaceCanvas.tsx +30 -2
  65. package/src/motion/typesurface/config.ts +29 -3
  66. package/src/motion/typesurface/construct.ts +828 -0
  67. package/src/motion/typesurface/engine.ts +31 -5
  68. package/src/motion/typesurface/index.ts +7 -3
  69. package/src/motion/typesurface/presets.ts +75 -3
  70. package/src/motion/typesurface/renderer.ts +77 -17
  71. package/src/motion/typesurface/shaders.ts +23 -1
  72. package/src/motion/typesurface/surfaces.ts +78 -1
  73. package/tools/ask/dist/components.mjs +4235 -908
  74. package/tools/ask/dist/render.js +589 -0
  75. package/tools/ask/mcp.mjs +261 -7
  76. package/tools/ask/render.mjs +248 -0
@@ -16,6 +16,7 @@
16
16
  import { DEFAULT_TYPE_SURFACE, TS_FOLD_KEYS, TS_WRAP_KEYS, type TSFold, type TSWrap, type TypeSurfaceConfig } from './config';
17
17
  import { glyphAt, morphGap, type TSTextLayout } from './layout';
18
18
  import { bend, bendDistance, crumpleCreases, crumpleField, cylinder, roll, rotate, smoothstep, type Bend, type Crease, type Roll, type Vec3 } from './ops';
19
+ import type { ResolvedMaterial } from './construct';
19
20
  import type { SurfaceDef } from './surfaces';
20
21
 
21
22
  const DEG = Math.PI / 180;
@@ -38,7 +39,9 @@ export type TextMesh = {
38
39
  indexVersion: number;
39
40
  };
40
41
 
41
- export type PropMesh = { positions: Float32Array; normals: Float32Array; attrs: Float32Array; indices: Uint32Array; vertexCount: number };
42
+ /** A solid's triangles that share one look: the wine bottle (`material: null`), or one part of a constructed object. */
43
+ export type PropRange = { start: number; count: number; material: ResolvedMaterial | null };
44
+ export type PropMesh = { positions: Float32Array; normals: Float32Array; attrs: Float32Array; indices: Uint32Array; vertexCount: number; ranges: PropRange[] };
42
45
 
43
46
  export type Guides = {
44
47
  folds: Float32Array[];
@@ -225,10 +228,16 @@ export function createGeometryEngine(def: SurfaceDef): GeometryEngine {
225
228
  };
226
229
  let textRect = '';
227
230
 
228
- const bottle = def.prop === 'bottle' ? bottleMesh(def.curveRadius - 0.0025, def.curveRadius) : null;
231
+ // The solid the sheet wraps: the wine bottle, or a constructed object's parts.
232
+ const bottle = def.prop === 'bottle' ? bottleMesh(def.curveRadius - 0.0025, def.curveRadius)
233
+ : def.solid ? { ...def.solid, attrs: new Float32Array(def.solid.count * 4) } : null;
229
234
  const prop: PropMesh | null = bottle
230
- ? { positions: new Float32Array(bottle.count * 3), normals: new Float32Array(bottle.count * 3), attrs: bottle.attrs, indices: bottle.indices, vertexCount: bottle.count }
235
+ ? {
236
+ positions: new Float32Array(bottle.count * 3), normals: new Float32Array(bottle.count * 3), attrs: bottle.attrs, indices: bottle.indices, vertexCount: bottle.count,
237
+ ranges: def.solid?.ranges ?? [{ start: 0, count: bottle.indices.length, material: null }],
238
+ }
231
239
  : null;
240
+ const solid = !!(def.prop || def.solid);
232
241
 
233
242
  const loose = def.looseSheet
234
243
  ? { positions: new Float32Array(12), normals: new Float32Array([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]), uvs: new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), indices: new Uint32Array([0, 2, 1, 1, 2, 3]) }
@@ -262,6 +271,12 @@ export function createGeometryEngine(def: SurfaceDef): GeometryEngine {
262
271
  if (F.roll) roll(p, F.roll);
263
272
  if (F.kappa) {
264
273
  cylinder(p, F.kappa);
274
+ if (def.radiusAt) {
275
+ // Follow the part's own radius at this height (a taper, a bulge), about the cylinder's axis.
276
+ const k = 1 + (def.radiusAt(p[1]) / def.curveRadius - 1) * F.d.curve, A = 1 / F.kappa;
277
+ p[0] *= k;
278
+ p[2] = (p[2] + A) * k - A;
279
+ }
265
280
  if (F.spin) {
266
281
  const R = def.curveRadius;
267
282
  const zz = p[2] + R, c = Math.cos(F.spin), s = Math.sin(F.spin);
@@ -313,6 +328,15 @@ export function createGeometryEngine(def: SurfaceDef): GeometryEngine {
313
328
  };
314
329
 
315
330
  const scratch: Vec3 = [0, 0, 0];
331
+ /** The solid turns with its label, about the wrap axis (a mug's handle comes round with the words). */
332
+ const spinSolid = (p: Vec3, direction = false) => {
333
+ if (!F.spin) return;
334
+ const R = direction ? 0 : def.curveRadius;
335
+ const zz = p[2] + R, c = Math.cos(F.spin), s = Math.sin(F.spin);
336
+ const xx = p[0] * c + zz * s;
337
+ p[2] = -p[0] * s + zz * c - R;
338
+ p[0] = xx;
339
+ };
316
340
 
317
341
  const evaluate: GeometryEngine['evaluate'] = (cfg, timeMs, layout, opts) => {
318
342
  const d = cfg.deformation;
@@ -347,7 +371,7 @@ export function createGeometryEngine(def: SurfaceDef): GeometryEngine {
347
371
  crumpleA: c > 0 ? c * (0.5 + 0.5 * smoothstep(0.5, 1, c)) : 0, shrink: c > 0 ? shrink : 1, bowl: 0.35 * smoothstep(0.55, 1, c),
348
372
  wrinkleA: d.wrinkle > 0 ? d.wrinkle * def.wrinkleDepth * 60 : 0,
349
373
  bends, roll: rl, kappa,
350
- spin: def.prop === 'bottle' ? d.curve * cfg.view.motion * 0.5 * Math.sin((2 * Math.PI * timeMs) / 10000) : 0,
374
+ spin: solid ? d.curve * cfg.view.motion * 0.5 * Math.sin((2 * Math.PI * timeMs) / 10000) : 0,
351
375
  rotA: d.tiltX * 0.6, rotB: -d.tiltY * 0.6, rotC: cfg.view.roll * DEG, shiftX: 0, shiftY: 0,
352
376
  creases: field?.creases ?? [],
353
377
  };
@@ -369,10 +393,12 @@ export function createGeometryEngine(def: SurfaceDef): GeometryEngine {
369
393
  if (prop && bottle) {
370
394
  for (let k = 0; k < bottle.count; k++) {
371
395
  scratch[0] = bottle.positions[k * 3]!; scratch[1] = bottle.positions[k * 3 + 1]!; scratch[2] = bottle.positions[k * 3 + 2]!;
396
+ spinSolid(scratch);
372
397
  rotate(scratch, F.rotA, F.rotB, F.rotC);
373
398
  prop.positions.set(scratch, k * 3);
374
399
  for (let a = 0; a < 3; a++) { const v = scratch[a]!; if (v < min[a]!) min[a] = v; if (v > max[a]!) max[a] = v; }
375
400
  scratch[0] = bottle.normals[k * 3]!; scratch[1] = bottle.normals[k * 3 + 1]!; scratch[2] = bottle.normals[k * 3 + 2]!;
401
+ spinSolid(scratch, true);
376
402
  rotate(scratch, F.rotA, F.rotB, F.rotC);
377
403
  prop.normals.set(scratch, k * 3);
378
404
  }
@@ -555,7 +581,7 @@ export function createGeometryEngine(def: SurfaceDef): GeometryEngine {
555
581
  guides = { folds, foldZones: zones, roll: rollLine, textOutline: outline, baselines, anchors: new Float32Array(anchors) };
556
582
  }
557
583
 
558
- const radius = def.prop === 'bottle' ? 1.45 : 0.5 * Math.hypot(W, def.height) * 1.04;
584
+ const radius = def.frameRadius ?? (def.prop === 'bottle' ? 1.45 : 0.5 * Math.hypot(W, def.height) * 1.04);
559
585
  return {
560
586
  def, nu, nv, uvs, indices,
561
587
  sheet: { positions: pos, normals: nrm, attrs },
@@ -3,14 +3,18 @@
3
3
  * crumples, rolls and finally prints into the material. `docs/TYPE-SURFACE.md`.
4
4
  *
5
5
  * text → layout (TextGeometry) → surface pipeline (SurfaceMapping) → deformation → material → renderer
6
+ *
7
+ * The text meets a built-in surface, or an object composed from primitives (`construct.ts`):
8
+ * `describeConstruction()` is the manifest an agent reads to build one.
6
9
  */
7
10
  export * from './config';
8
11
  export { stageEnvelope, envelopes, configAt, stageLabels, activeStage } from './timeline';
9
12
  export { bend, bendDistance, roll, cylinder, crumpleCreases, crumpleField, rotate, smoothstep, type Bend, type Roll, type Crease, type Vec3 } from './ops';
10
- export { SURFACES, registerSurface, getSurface, hasSurface, listSurfaces, type SurfaceDef, type SurfaceState, type SurfaceShading, type TableStyle } from './surfaces';
13
+ export * from './construct';
14
+ export { SURFACES, registerSurface, getSurface, hasSurface, listSurfaces, objectSurface, surfaceFor, type SurfaceDef, type SurfaceState, type SurfaceShading, type TableStyle } from './surfaces';
11
15
  export { layoutText, estimateMeasurer, morphGap, glyphShift, glyphAt, type TSMeasurer, type TSGlyph, type TSLine, type TSTextLayout } from './layout';
12
- export { createGeometryEngine, textVertexOnSheet, type GeometryEngine, type SceneGeometry, type TextMesh, type PropMesh, type Guides } from './engine';
13
- export { TYPE_SURFACE_PRESETS, findTypeSurfacePreset, applySurfaceState, switchSurface, createTypeSurface, type TypeSurfacePreset, type TypeSurfaceShorthand } from './presets';
16
+ export { createGeometryEngine, textVertexOnSheet, type GeometryEngine, type SceneGeometry, type TextMesh, type PropMesh, type PropRange, type Guides } from './engine';
17
+ export { TYPE_SURFACE_PRESETS, findTypeSurfacePreset, applySurfaceState, switchSurface, withObject, fitObjectText, createTypeSurface, type TypeSurfacePreset, type TypeSurfaceShorthand } from './presets';
14
18
  export { TS_FONTS, TS_FONT_CATEGORIES, findTypeSurfaceFont, typeSurfaceFontsUrl, type TSFont, type TSFontCategory } from './fonts';
15
19
  export { TypeSurfaceRenderer, drawDebugOverlay, cameraFor, type TSDebug } from './renderer';
16
20
  export { TypeSurfaceCanvas, type TypeSurfaceCanvasProps } from './TypeSurfaceCanvas';
@@ -7,7 +7,9 @@
7
7
  */
8
8
  import { findTypeSurfaceFont } from './fonts';
9
9
  import { DEFAULT_TYPE_SURFACE, REST_DEFORMATION, resolveTypeSurface, type TSStage, type TSSurfaceType, type TypeSurfaceConfig, type TypeSurfaceConfigInput } from './config';
10
- import { getSurface } from './surfaces';
10
+ import { createObject, OBJECT_FRONT_ARC, type TSObjectMaterialId, type TSObjectSpec } from './construct';
11
+ import { estimateMeasurer, layoutText, type TSMeasurer } from './layout';
12
+ import { getSurface, objectSurface, surfaceFor } from './surfaces';
11
13
 
12
14
  const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
13
15
  function merge<T>(base: T, over: unknown): T {
@@ -22,7 +24,7 @@ function merge<T>(base: T, over: unknown): T {
22
24
  * change; the text, and how far it has wrapped and morphed, do not.
23
25
  */
24
26
  export function applySurfaceState(config: TypeSurfaceConfig, stateId: string): TypeSurfaceConfig {
25
- const def = getSurface(config.surface.type);
27
+ const def = surfaceFor(config);
26
28
  const state = def.states[stateId];
27
29
  if (!state) return config;
28
30
  const deformation = merge(REST_DEFORMATION, state.deformation);
@@ -40,7 +42,7 @@ export function applySurfaceState(config: TypeSurfaceConfig, stateId: string): T
40
42
  */
41
43
  export function switchSurface(config: TypeSurfaceConfig, type: TSSurfaceType | string): TypeSurfaceConfig {
42
44
  const def = getSurface(type);
43
- const next: TypeSurfaceConfig = { ...config, surface: { ...config.surface, type: type as TSSurfaceType, color: null }, material: { ...config.material, ...def.material } };
45
+ const next: TypeSurfaceConfig = { ...config, object: null, surface: { ...config.surface, type: type as TSSurfaceType, color: null }, material: { ...config.material, ...def.material } };
44
46
  return applySurfaceState(next, def.defaultState);
45
47
  }
46
48
 
@@ -55,6 +57,49 @@ const on = (type: TSSurfaceType, input: TypeSurfaceConfigInput): TypeSurfaceConf
55
57
  return resolveTypeSurface({ ...input, surface: { type, state: def.defaultState, seed: 7, color: null, ...input.surface } }, { ...base, deformation: REST_DEFORMATION });
56
58
  };
57
59
 
60
+ /**
61
+ * Put the text on a constructed object: the object replaces the surface, the text wraps the part it
62
+ * names and prints in its own colour (not multiplied into the part's). The words and type are kept.
63
+ */
64
+ export function withObject(config: TypeSurfaceConfig, object: TSObjectSpec): TypeSurfaceConfig {
65
+ const def = objectSurface(object);
66
+ return {
67
+ ...config, object,
68
+ surface: { ...config.surface, type: 'object', state: def.defaultState, color: null },
69
+ typography: { ...config.typography, blendMode: 'normal' },
70
+ placement: { ...config.placement, x: 0.5, y: 0.5, rotation: 0 },
71
+ deformation: { ...merge(REST_DEFORMATION, def.states[def.defaultState]!.deformation), wrap: 1 },
72
+ material: { ...config.material, ...def.material, blend: 1, gloss: 0.2 },
73
+ view: { ...config.view, roll: 0 },
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Size the text to the object: as large as fits the part's front face (`OBJECT_FRONT_ARC` of its
79
+ * circumference) and the text area's height, on one block that never wraps. Without an object the
80
+ * config is returned as it is. Pass the canvas measurer in a browser for exact widths.
81
+ */
82
+ export function fitObjectText(config: TypeSurfaceConfig, measure: TSMeasurer = estimateMeasurer): TypeSurfaceConfig {
83
+ if (!config.object || !config.text.trim()) return config;
84
+ const def = objectSurface(config.object);
85
+ const arc = (def.width / def.curveRadius) * (180 / Math.PI);
86
+ const wide = 1000 * Math.min(1, OBJECT_FRONT_ARC / arc) * 0.95;
87
+ const high = 1000 * (def.height / def.width) * 0.8;
88
+ const at100 = layoutText(config.text, { ...config.typography, fontSize: 100 }, 0, measure);
89
+ const fontSize = Math.max(12, Math.min(400, Math.floor(100 * Math.min(wide / at100.width, high / at100.height))));
90
+ return { ...config, typography: { ...config.typography, fontSize }, placement: { ...config.placement, maxWidth: 0 } };
91
+ }
92
+
93
+ /** Near-white or near-black, whichever reads on a colour. */
94
+ const inkOn = (hex: string): string => {
95
+ const h = hex.length === 4 ? hex.slice(1).split('').map((c) => c + c).join('') : hex.slice(1);
96
+ const n = parseInt(h, 16);
97
+ return 0.299 * ((n >> 16) & 255) + 0.587 * ((n >> 8) & 255) + 0.114 * (n & 255) > 140 ? '#1B1D20' : '#FFFFFF';
98
+ };
99
+
100
+ const onObject = (object: TSObjectSpec, input: TypeSurfaceConfigInput): TypeSurfaceConfig =>
101
+ resolveTypeSurface(input, withObject({ ...DEFAULT_TYPE_SURFACE, deformation: REST_DEFORMATION }, object));
102
+
58
103
  const ANTON = "'Anton', Impact, sans-serif";
59
104
  const PLAYFAIR = "'Playfair Display', Georgia, serif";
60
105
 
@@ -167,6 +212,22 @@ export const TYPE_SURFACE_PRESETS: TypeSurfacePreset[] = [
167
212
  animation: { duration: 8000, stages: [stage('crumple', 0.15, 0.7, 'crush')] },
168
213
  }),
169
214
  },
215
+ {
216
+ id: 'object-tumbler', name: 'Tumbler · Built from parts', description: 'No tumbler exists in the library: a hollow tapered cylinder, a ring for the rim and a short cylinder for the foot. The name wraps the taper.',
217
+ config: onObject(createObject('tumbler'), {
218
+ text: 'Shekhar', typography: { fontFamily: "'Archivo Black', 'Arial Black', sans-serif", fontSize: 120, letterSpacing: 0, color: '#F4F1EA' },
219
+ view: { motion: 0.8 },
220
+ animation: { duration: 8000, stages: [stage('curve', 0.05, 0.45, 'wrap round'), stage('blend', 0.45, 0.85, 'print')] },
221
+ }),
222
+ },
223
+ {
224
+ id: 'object-mug', name: 'Mug · Built from parts', description: 'A hollow cylinder and a tube along a path for the handle, glazed black; the name is printed in white.',
225
+ config: onObject(createObject('mug'), {
226
+ text: 'Tushar', typography: { fontFamily: PLAYFAIR, fontWeight: 900, fontSize: 150, letterSpacing: 0, color: '#FFFFFF' },
227
+ view: { motion: 0.8 },
228
+ animation: { duration: 8000, stages: [stage('curve', 0.05, 0.45, 'wrap round'), stage('blend', 0.45, 0.85, 'print')] },
229
+ }),
230
+ },
170
231
  ];
171
232
 
172
233
  export function findTypeSurfacePreset(id: string): TypeSurfacePreset | undefined {
@@ -183,17 +244,28 @@ export type TypeSurfaceShorthand = {
183
244
  material?: TypeSurfaceConfig['material']['type'];
184
245
  /** One of the preset ids to start from. */
185
246
  preset?: string;
247
+ /** Instead of a surface: a recipe name (`TS_RECIPES`), or a whole object spec built from primitives. */
248
+ object?: string | TSObjectSpec;
249
+ /** With a recipe name: the object's colour and material. The text is then sized to the object (`fitObjectText`) and, with no `color`, inked to read on it. */
250
+ objectColor?: string;
251
+ objectMaterial?: TSObjectMaterialId;
186
252
  };
187
253
 
188
254
  /** Build an experiment from shorthand: preset → surface → state → text, font, colour, material. */
189
255
  export function createTypeSurface(s: TypeSurfaceShorthand = {}): TypeSurfaceConfig {
190
256
  let cfg = findTypeSurfacePreset(s.preset ?? '')?.config ?? TYPE_SURFACE_PRESETS[0]!.config;
191
257
  if (s.surface && s.surface !== cfg.surface.type) cfg = switchSurface(cfg, s.surface);
258
+ if (s.object) cfg = withObject(cfg, typeof s.object === 'string' ? createObject(s.object, { color: s.objectColor, material: s.objectMaterial }) : s.object);
192
259
  if (s.state) cfg = applySurfaceState(cfg, s.state);
193
260
  if (s.text !== undefined) cfg = { ...cfg, text: s.text };
194
261
  const font = s.font ? findTypeSurfaceFont(s.font) : undefined;
195
262
  if (font) cfg = { ...cfg, typography: { ...cfg.typography, fontFamily: font.family, fontWeight: font.weights.includes(cfg.typography.fontWeight) ? cfg.typography.fontWeight : font.weights[font.weights.length - 1]! } };
196
263
  if (s.color && /^#[0-9a-f]{6}$/i.test(s.color)) cfg = { ...cfg, typography: { ...cfg.typography, color: s.color.toUpperCase() } };
197
264
  if (s.material) cfg = { ...cfg, material: { ...cfg.material, type: s.material } };
265
+ if (cfg.object && s.object) {
266
+ // On an object the words start legible: sized to its front face, in an ink that reads on it.
267
+ if (!s.color) cfg = { ...cfg, typography: { ...cfg.typography, color: inkOn(objectSurface(cfg.object).color) } };
268
+ cfg = fitObjectText(cfg);
269
+ }
198
270
  return cfg;
199
271
  }
@@ -10,8 +10,9 @@ import { ART_FONTS, paintGrain, paintSurfaceArt } from './art';
10
10
  import { TS_BLEND_MODES, type TypeSurfaceConfig } from './config';
11
11
  import { createGeometryEngine, type GeometryEngine, type SceneGeometry } from './engine';
12
12
  import { layoutText, morphGap, glyphShift, type TSTextLayout } from './layout';
13
+ import { findTypeSurfaceFont, typeSurfaceFontsUrl } from './fonts';
13
14
  import { DEPTH_FS, DEPTH_VS, SURFACE_FS, SURFACE_VS, TEXT_FS, TEXT_VS } from './shaders';
14
- import { getSurface, type SurfaceDef } from './surfaces';
15
+ import { surfaceFor, type SurfaceDef } from './surfaces';
15
16
  import { canvasMeasurer, cssFont, rasterText } from './text';
16
17
  import type { Vec3 } from './ops';
17
18
 
@@ -104,11 +105,14 @@ export class TypeSurfaceRenderer {
104
105
  private shadowFbo!: WebGLFramebuffer;
105
106
  private shadowTex!: WebGLTexture;
106
107
  private shadowSize = 2048;
107
- private indexVersions = new Map<string, number>();
108
+ private indexVersions = new Map<string, number | string>();
108
109
  private viewProj = m4();
109
110
  private fontsVersion = 0;
110
111
  private lastGeometry: SceneGeometry | null = null;
111
112
  private propUv: Float32Array | null = null;
113
+ /** Bumped when an engine is (re)built, so static buffers (uvs, triangles) are uploaded again. */
114
+ private geometryVersion = 1;
115
+ private uvVersions = new Map<string, number>();
112
116
  private onFonts = () => { this.fontsVersion++; };
113
117
 
114
118
  constructor(private canvas: HTMLCanvasElement) {
@@ -210,7 +214,7 @@ export class TypeSurfaceRenderer {
210
214
  gl.enableVertexAttribArray(loc);
211
215
  gl.vertexAttribPointer(loc, size, gl.FLOAT, false, 0, 0);
212
216
  }
213
- private elements(key: string, data: Uint32Array, count: number, version: number) {
217
+ private elements(key: string, data: Uint32Array, count: number, version: number | string) {
214
218
  const gl = this.gl;
215
219
  if (this.indexVersions.get(key) !== version || !this.bufs.has(key)) {
216
220
  this.buffer(key, data, gl.ELEMENT_ARRAY_BUFFER, count);
@@ -220,17 +224,25 @@ export class TypeSurfaceRenderer {
220
224
 
221
225
  private engine(def: SurfaceDef): GeometryEngine {
222
226
  let e = this.engines.get(def.id);
223
- if (!e) { e = createGeometryEngine(def); this.engines.set(def.id, e); }
227
+ // The engine is built for one definition: a re-registered surface, or an edited object, gets a new one.
228
+ if (!e || e.def !== def) {
229
+ if (def.solid) for (const [k, v] of this.engines) if (v.def.solid) this.engines.delete(k);
230
+ e = createGeometryEngine(def);
231
+ this.engines.set(def.id, e);
232
+ this.geometryVersion++;
233
+ }
224
234
  return e;
225
235
  }
226
236
 
227
237
  private artFor(def: SurfaceDef, seed: number, color: string) {
228
- const key = `${def.id}:${seed}:${color}:${this.fontsVersion}`;
238
+ // Every constructed object shares one slot, so editing one doesn't pile up textures.
239
+ const slot = def.solid ? 'object' : def.id;
240
+ const key = `${slot}:${def.solid ? `${def.width.toFixed(4)}:${def.height.toFixed(4)}:` : ''}${seed}:${color}:${this.fontsVersion}`;
229
241
  let a = this.art.get(key);
230
242
  if (!a) {
231
243
  const aspect = def.height / def.width;
232
244
  const { front, back, inner } = paintSurfaceArt(def.art, seed, aspect, color);
233
- for (const [k, v] of this.art) if (k.startsWith(`${def.id}:`)) { this.gl.deleteTexture(v.front); this.gl.deleteTexture(v.back); this.gl.deleteTexture(v.inner); this.art.delete(k); }
245
+ for (const [k, v] of this.art) if (k.startsWith(`${slot}:`)) { this.gl.deleteTexture(v.front); this.gl.deleteTexture(v.back); this.gl.deleteTexture(v.inner); this.art.delete(k); }
234
246
  a = { front: this.texture(front, true), back: this.texture(back, true), inner: this.texture(inner, true), aspect };
235
247
  this.art.set(key, a);
236
248
  }
@@ -271,6 +283,41 @@ export class TypeSurfaceRenderer {
271
283
 
272
284
  get geometry(): SceneGeometry | null { return this.lastGeometry; }
273
285
 
286
+ /**
287
+ * Resolves once everything `config` draws with has loaded: its text font and the artwork's faces.
288
+ * It waits for stylesheets still loading, and (unless `loadFonts` is false) links the lab's Google
289
+ * Fonts stylesheet when the font is one of `TS_FONTS` and the page has not declared it. Any other
290
+ * undeclared font resolves with its fallback. Render after it.
291
+ */
292
+ async whenReady(config: TypeSurfaceConfig, loadFonts = true): Promise<void> {
293
+ const fonts = document.fonts;
294
+ if (!fonts?.load) return;
295
+ const font = cssFont(config.typography, 100);
296
+ const load = () => fonts.load(font, config.text || 'A').catch(() => [] as FontFace[]);
297
+ const sheetLoaded = (l: HTMLLinkElement) => (l.sheet ? Promise.resolve() : new Promise<void>((done) => {
298
+ l.addEventListener('load', () => done(), { once: true });
299
+ l.addEventListener('error', () => done(), { once: true });
300
+ }));
301
+ // No face for the font yet: `load` resolves empty, which would pass a fallback off as ready. A
302
+ // stylesheet may still be on its way; failing that, a lab font is fetched from its own stylesheet.
303
+ if ((await load()).length === 0) {
304
+ await Promise.all(Array.from(document.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]')).map(sheetLoaded));
305
+ if ((await load()).length === 0 && loadFonts && findTypeSurfaceFont(config.typography.fontFamily.split(',')[0]!)) {
306
+ const href = typeSurfaceFontsUrl();
307
+ let link = Array.from(document.querySelectorAll<HTMLLinkElement>('link[rel="stylesheet"]')).find((l) => l.href === href);
308
+ if (!link) {
309
+ link = document.createElement('link');
310
+ link.rel = 'stylesheet'; link.href = href;
311
+ document.head.appendChild(link);
312
+ }
313
+ await sheetLoaded(link);
314
+ await load();
315
+ }
316
+ }
317
+ await Promise.all(ART_FONTS.map((f) => fonts.load(f).catch(() => undefined)));
318
+ await fonts.ready;
319
+ }
320
+
274
321
  /** Draw `config` (already at its timeline moment) at `timeMs`. */
275
322
  render(config: TypeSurfaceConfig, timeMs: number, debug: TSDebug = {}): SceneGeometry | null {
276
323
  if (!this.ok) return null;
@@ -280,11 +327,15 @@ export class TypeSurfaceRenderer {
280
327
  const W = Math.max(2, Math.round(cv.clientWidth * dpr)), H = Math.max(2, Math.round(cv.clientHeight * dpr));
281
328
  if (cv.width !== W || cv.height !== H) { cv.width = W; cv.height = H; }
282
329
 
283
- const def = getSurface(config.surface.type);
330
+ const def = surfaceFor(config);
331
+ // Buffers are keyed by surface; every constructed object shares one set, re-uploaded when it changes.
332
+ const bk = def.solid ? 'object' : def.id;
284
333
  const layout = this.layoutFor(config);
285
334
  const g = this.engine(def).evaluate(config, timeMs, layout, { guides: !!(debug.folds || debug.bounds || debug.baseline || debug.controls) });
286
335
  this.lastGeometry = g;
287
- const art = this.artFor(def, config.surface.seed, def.prop ? def.color : config.surface.color ?? def.color);
336
+ const gv = this.geometryVersion;
337
+ const sheetOn = def.sheet !== 'hidden';
338
+ const art = this.artFor(def, config.surface.seed, def.prop || def.solid ? def.color : config.surface.color ?? def.color);
288
339
  const textTex = layout && g.text ? this.textTexture(config, layout) : null;
289
340
 
290
341
  const cam = cameraFor(config, def, g, W / H);
@@ -331,14 +382,14 @@ export class TypeSurfaceRenderer {
331
382
  const bindSheet = (positionsKey: string, positions: Float32Array) => {
332
383
  this.attr(0, positionsKey, positions, 3);
333
384
  this.attrKeep(1, 'sheet.n', 3);
334
- this.attrKeep(2, `sheet.uv:${def.id}`, 2);
385
+ this.attrKeep(2, `sheet.uv:${bk}`, 2);
335
386
  this.attrKeep(3, 'sheet.a', 4);
336
387
  gl.disableVertexAttribArray(4);
337
- this.elements(`sheet.i:${def.id}`, g.indices, g.indices.length, 1);
388
+ this.elements(`sheet.i:${bk}`, g.indices, g.indices.length, gv);
338
389
  };
339
390
  this.buffer('sheet.n', g.sheet.normals, gl.ARRAY_BUFFER);
340
391
  this.buffer('sheet.a', g.sheet.attrs, gl.ARRAY_BUFFER);
341
- if (!this.bufs.has(`sheet.uv:${def.id}`)) this.buffer(`sheet.uv:${def.id}`, g.uvs, gl.ARRAY_BUFFER);
392
+ if (this.uvVersions.get(bk) !== gv) { this.buffer(`sheet.uv:${bk}`, g.uvs, gl.ARRAY_BUFFER); this.uvVersions.set(bk, gv); }
342
393
  const bindProp = () => {
343
394
  const pr = g.prop!;
344
395
  this.attr(0, 'prop.p', pr.positions, 3);
@@ -347,7 +398,7 @@ export class TypeSurfaceRenderer {
347
398
  this.attr(2, 'prop.uv', this.propUv, 2);
348
399
  this.attr(3, 'prop.a', pr.attrs, 4);
349
400
  gl.disableVertexAttribArray(4);
350
- this.elements(`prop.i:${def.id}`, pr.indices, pr.indices.length, 1);
401
+ this.elements(`prop.i:${bk}`, pr.indices, pr.indices.length, gv);
351
402
  };
352
403
  const drawSheetGeom = () => gl.drawElements(gl.TRIANGLES, g.indices.length, gl.UNSIGNED_INT, 0);
353
404
  const bindText = () => {
@@ -357,7 +408,7 @@ export class TypeSurfaceRenderer {
357
408
  this.attr(2, 'text.uv', t.uvs, 2, t.vertexCount);
358
409
  this.attr(3, 'text.a', t.attrs, 4, t.vertexCount);
359
410
  this.attr(4, 'text.tuv', t.tuv, 2, t.vertexCount);
360
- this.elements('text.i', t.indices, t.indexCount, t.indexVersion * 100000 + t.indexCount);
411
+ this.elements('text.i', t.indices, t.indexCount, `${def.id}:${gv}:${t.indexVersion}:${t.indexCount}`);
361
412
  };
362
413
  const bindQuad = (key: string, pos: Float32Array, uvs: Float32Array) => {
363
414
  this.attr(0, `${key}.p`, pos, 3);
@@ -380,7 +431,7 @@ export class TypeSurfaceRenderer {
380
431
  gl.useProgram(dp.p);
381
432
  gl.uniformMatrix4fv(this.loc(dp, 'uVP'), false, lightVP);
382
433
  gl.uniform1i(this.loc(dp, 'uIsText'), 0);
383
- if (!debug.noShadows) { bindSheet('sheet.p', g.sheet.positions); drawSheetGeom(); }
434
+ if (!debug.noShadows && sheetOn) { bindSheet('sheet.p', g.sheet.positions); drawSheetGeom(); }
384
435
  if (!debug.noShadows) g.stack.forEach((s, i) => { bindSheet(`stack${i}.p`, s); drawSheetGeom(); });
385
436
  if (g.prop && !debug.noShadows) { bindProp(); gl.drawElements(gl.TRIANGLES, g.prop.indices.length, gl.UNSIGNED_INT, 0); }
386
437
  if (layout && g.text && textTex && config.material.shadow > 0.02 && !debug.noShadows) {
@@ -444,13 +495,22 @@ export class TypeSurfaceRenderer {
444
495
  g.stack.forEach((s, i) => { bindSheet(`stack${i}.p`, s); drawSheetGeom(); });
445
496
  gl.uniform1i(this.loc(sp, 'uDebug'), debug.deformation ? 1 : debug.mapping ? 2 : 0);
446
497
  tex(art.front, art.back);
447
- bindSheet('sheet.p', g.sheet.positions);
448
- drawSheetGeom();
498
+ if (sheetOn) { bindSheet('sheet.p', g.sheet.positions); drawSheetGeom(); }
449
499
  if (g.prop) {
450
500
  gl.uniform1i(this.loc(sp, 'uMode'), 2);
451
501
  gl.uniform1i(this.loc(sp, 'uDebug'), 0);
452
502
  bindProp();
453
- gl.drawElements(gl.TRIANGLES, g.prop.indices.length, gl.UNSIGNED_INT, 0);
503
+ // One draw per part: each has its own material.
504
+ for (const r of g.prop.ranges) {
505
+ const mt = r.material;
506
+ gl.uniform1i(this.loc(sp, 'uPartOn'), mt ? 1 : 0);
507
+ if (mt) {
508
+ gl.uniform3fv(this.loc(sp, 'uPropColor'), hexRgb(mt.color));
509
+ gl.uniform4f(this.loc(sp, 'uPart'), mt.metalness, mt.roughness, mt.reflection, mt.gain);
510
+ }
511
+ gl.drawElements(gl.TRIANGLES, r.count, gl.UNSIGNED_INT, r.start * 4);
512
+ }
513
+ gl.uniform1i(this.loc(sp, 'uPartOn'), 0);
454
514
  gl.uniform1i(this.loc(sp, 'uMode'), 0);
455
515
  }
456
516
 
@@ -57,6 +57,12 @@ vec3 envAt(vec3 r) {
57
57
  float strip = smoothstep(0.975, 0.992, dot(normalize(r - ax * along), sd)) * (1.0 - smoothstep(0.4, 0.62, abs(along)));
58
58
  return base + vec3(1.0, 0.98, 0.95) * key * 3.2 + vec3(0.95, 0.97, 1.0) * strip * 2.4;
59
59
  }
60
+ /** The same studio seen in a rough surface: the lights spread into a broad glow. */
61
+ vec3 envSoft(vec3 r) {
62
+ vec3 base = mix(vec3(0.07, 0.07, 0.075), vec3(0.36, 0.37, 0.39), smoothstep(-0.6, 1.0, r.z));
63
+ float key = smoothstep(0.3, 1.0, dot(r, uLightDir));
64
+ return base + vec3(1.0, 0.98, 0.95) * key * key * 0.9;
65
+ }
60
66
  float fresnel(float ndv, float f0) { return f0 + (1.0 - f0) * pow(1.0 - ndv, 5.0); }
61
67
 
62
68
  float hash12(vec2 p) { vec3 p3 = fract(vec3(p.xyx) * 0.1031); p3 += dot(p3, p3.yzx + 33.33); return fract((p3.x + p3.y) * p3.z); }
@@ -79,7 +85,9 @@ void main() { vW = aPos; vN = aNrm; vUv = aUv; vAttr = aAttr; gl_Position = uVP
79
85
 
80
86
  export const SURFACE_FS = COMMON + /* glsl */ `
81
87
  in vec3 vW; in vec3 vN; in vec2 vUv; in vec4 vAttr;
82
- uniform int uMode; // 0 sheet, 1 table, 2 prop (bottle)
88
+ uniform int uMode; // 0 sheet, 1 table, 2 prop (the bottle, or a constructed part)
89
+ uniform int uPartOn; // 1: a constructed part, shaded by uPart; 0: the wine bottle
90
+ uniform vec4 uPart; // metalness, roughness, reflection, albedo gain
83
91
  uniform int uShading; // 0 paper, 1 label, 2 film, 3 fabric
84
92
  uniform vec3 uPropColor;
85
93
  uniform vec3 uCapColor;
@@ -119,6 +127,20 @@ void main() {
119
127
  return;
120
128
  }
121
129
  vec3 V = normalize(uEye - vW);
130
+ if (uMode == 2 && uPartOn == 1) {
131
+ if (!gl_FrontFacing) n = -n;
132
+ float ndv = max(dot(n, V), 0.0);
133
+ vec3 alb = toLin(uPropColor) * uPart.w;
134
+ float metal = uPart.x, rough = uPart.y;
135
+ vec3 body = shade(alb * (1.0 - metal), n, vW, 0.0, uPart.z * (1.0 - rough) * 1.2, rough, 0.0);
136
+ // A dielectric reflects the studio through its Fresnel term; a metal reflects it in its own colour.
137
+ vec3 F = mix(vec3(fresnel(ndv, 0.05)), alb + (1.0 - alb) * pow(1.0 - ndv, 5.0), metal);
138
+ vec3 R = reflect(-V, n);
139
+ vec3 env = mix(envAt(R), envSoft(R), smoothstep(0.08, 0.5, rough));
140
+ col = body + env * F * uPart.z;
141
+ outColor = vec4(toSrgb(tone(col)), 1.0);
142
+ return;
143
+ }
122
144
  if (uMode == 2) {
123
145
  float ndv = max(dot(n, V), 0.0);
124
146
  vec3 R = reflect(-V, n);
@@ -10,7 +10,8 @@
10
10
  * Its `id` becomes a valid `surface.type`, and the renderer paints it with the named `art` (or a
11
11
  * plain colour if it has no painter). Pure; no React.
12
12
  */
13
- import type { TSDeformation, TSMaterial, TSSurfaceType, TSView } from './config';
13
+ import type { TSDeformation, TSMaterial, TSSurfaceType, TSView, TypeSurfaceConfig } from './config';
14
+ import { buildObject, createObject, sanitizeObject, type PartRange, type TSObjectSpec } from './construct';
14
15
  import { smoothstep, type Vec3 } from './ops';
15
16
 
16
17
  /** How the substrate itself shades. The renderer maps each to a lighting model. */
@@ -51,6 +52,17 @@ export type SurfaceDef = {
51
52
  looseSheet?: boolean;
52
53
  /** A solid object the surface is wrapped on. */
53
54
  prop?: 'bottle';
55
+ /**
56
+ * A constructed object the surface is wrapped on (`objectSurface`): its mesh, already in the
57
+ * sheet's frame (the wrap axis parallel to y at z = −curveRadius), one triangle range per part.
58
+ */
59
+ solid?: { positions: Float32Array; normals: Float32Array; indices: Uint32Array; count: number; ranges: PartRange[] };
60
+ /** The wrapped sheet's radius at a height y (world): it follows a tapered or bulged part. */
61
+ radiusAt?: (y: number) => number;
62
+ /** `hidden`: the sheet is not drawn, so the text reads as printed straight onto the object. */
63
+ sheet?: 'visible' | 'hidden';
64
+ /** Radius that frames the object (camera fit); sheets work theirs out from their size. */
65
+ frameRadius?: number;
54
66
  /** How much fine wrinkles lift the surface, as a share of its width. */
55
67
  wrinkleDepth: number;
56
68
  /** The surface's own shape, before the shared operators. `x, y` are sheet units (1 wide). */
@@ -221,3 +233,68 @@ export function hasSurface(id: string): boolean {
221
233
  export function listSurfaces(): SurfaceDef[] {
222
234
  return [...REGISTRY.values()];
223
235
  }
236
+
237
+ /* ---------------- constructed objects ---------------- */
238
+
239
+ /** The gap between a part and the text area wrapped on it: enough depth to keep them apart. */
240
+ const OBJECT_GAP = 0.0025;
241
+ const OBJECTS = new Map<string, SurfaceDef>();
242
+
243
+ /**
244
+ * A constructed object as a surface: the parts become the solid, and a sheet the size of the text
245
+ * area wraps the part that carries the text, following its radius at every height. The sheet is
246
+ * hidden unless the object asks for a paper label, so the words read as printed on the object. An
247
+ * invalid spec is repaired first (`sanitizeObject`), so this always returns something drawable.
248
+ */
249
+ export function objectSurface(input: TSObjectSpec): SurfaceDef {
250
+ const key = JSON.stringify(input);
251
+ const hit = OBJECTS.get(key);
252
+ if (hit) return hit;
253
+ const spec = sanitizeObject(input).object ?? sanitizeObject(createObject('')).object!;
254
+ const built = buildObject(spec);
255
+ const { host } = built;
256
+ const hostH = host.y1 - host.y0;
257
+ const height = Math.max(0.02, spec.text.height * hostH);
258
+ // Keep the text area on the part.
259
+ const yc = Math.min(host.y1 - height / 2, Math.max(host.y0 + height / 2, host.y0 + spec.text.y * hostH));
260
+ const R = host.radiusAt(yc) + OBJECT_GAP;
261
+ const width = (spec.text.arc * Math.PI / 180) * R;
262
+ // Into the sheet's frame: the text area's centre at the origin, the axis behind it.
263
+ const positions = new Float32Array(built.positions);
264
+ for (let k = 0; k < built.count; k++) { positions[k * 3 + 1] = positions[k * 3 + 1]! - yc; positions[k * 3 + 2] = positions[k * 3 + 2]! - R; }
265
+ const { min, max } = built.bounds;
266
+ const nu = Math.round(Math.min(260, Math.max(96, spec.text.arc * 1.1)));
267
+ const def: SurfaceDef = {
268
+ id: `object:${hashString(key)}`, name: spec.name, blurb: `Built from ${spec.parts.map((p) => p.shape).join(' + ')}.`,
269
+ width, height, grid: [nu, Math.round(Math.min(220, Math.max(24, (nu * height) / width)))],
270
+ art: 'plain', shading: 'label', table: spec.scene.table, backdrop: spec.scene.backdrop,
271
+ color: spec.text.label ?? host.color,
272
+ material: { type: 'print', halftone: 0, grainInfluence: spec.text.label ? 0.25 : 0.05, paperInfluence: spec.text.label ? 0.35 : 0.15 },
273
+ curveRadius: R, curveMode: 'cylinder', wrinkleDepth: 0.004,
274
+ solid: { positions, normals: built.normals, indices: built.indices, count: built.count, ranges: built.ranges },
275
+ radiusAt: (y) => host.radiusAt(y + yc) + OBJECT_GAP,
276
+ sheet: spec.text.label ? 'visible' : 'hidden',
277
+ frameRadius: 0.5 * Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]) * 1.06,
278
+ states: {
279
+ wrapped: s('Standing', { curve: 1 }),
280
+ lying: s('Lying on the table', { curve: 1 }, { roll: -28 }),
281
+ turned: s('Turned away', { curve: 1, tiltX: 0.5 }),
282
+ tipped: s('Tipped toward you', { curve: 1, tiltY: -0.7 }),
283
+ },
284
+ defaultState: 'wrapped', camera: { flat: 0, deep: 48 },
285
+ };
286
+ if (OBJECTS.size >= 12) OBJECTS.delete(OBJECTS.keys().next().value!);
287
+ OBJECTS.set(key, def);
288
+ return def;
289
+ }
290
+
291
+ function hashString(v: string): string {
292
+ let h = 0x811c9dc5;
293
+ for (let i = 0; i < v.length; i++) h = Math.imul(h ^ v.charCodeAt(i), 0x01000193);
294
+ return (h >>> 0).toString(36);
295
+ }
296
+
297
+ /** What a config's text meets: its constructed `object` when it has one, else the surface it names. */
298
+ export function surfaceFor(config: Pick<TypeSurfaceConfig, 'surface' | 'object'>): SurfaceDef {
299
+ return config.object ? objectSurface(config.object) : getSurface(config.surface.type);
300
+ }