@rooms.sh/sdk 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -6,6 +6,15 @@ type ColorValue = number | `#${string}`;
6
6
  type WallName = "north" | "south" | "east" | "west";
7
7
  type MaterialSide = "front" | "back" | "double";
8
8
  type TextureRef = string;
9
+ declare const MAX_TEXTURE_DATA_URL_BYTES: number;
10
+ declare const MAX_GEOMETRY_TESSELLATION_SEGMENTS = 256;
11
+ declare const MAX_GEOMETRY_DETAIL = 6;
12
+ declare const MAX_GEOMETRY_POINTS = 4096;
13
+ declare const MAX_ESTIMATED_GEOMETRY_VERTICES = 500000;
14
+ declare const MIN_CEILING_GRID_SPACING = 0.05;
15
+ declare const MAX_CEILING_GRID_LINES = 256;
16
+ declare const MAX_RENDERED_OBJECTS = 10000;
17
+ declare const MAX_EXPANDED_POINT_LIGHTS = 2000;
9
18
  interface Transform {
10
19
  position?: Vec3;
11
20
  rotation?: Euler3;
@@ -286,10 +295,24 @@ interface RoomDefinition {
286
295
  }
287
296
  interface ValidationOptions {
288
297
  maxItems?: number;
298
+ maxItemNodes?: number;
289
299
  maxInstances?: number;
290
300
  maxLights?: number;
291
301
  maxShellExtent?: number;
302
+ maxCustomGeometryValues?: number;
303
+ maxShapePoints?: number;
304
+ maxGeometryPoints?: number;
305
+ maxGeometryTessellationSegments?: number;
306
+ maxGeometryDetail?: number;
307
+ maxEstimatedGeometryVertices?: number;
308
+ maxTextureDataUrlBytes?: number;
309
+ minCeilingGridSpacing?: number;
310
+ maxCeilingGridLines?: number;
311
+ maxRenderedObjects?: number;
312
+ maxExpandedPointLights?: number;
292
313
  }
314
+ declare function isDataTextureUrl(source: string): boolean;
315
+ declare function validateTextureRef(source: TextureRef, label: string, options?: Pick<Required<ValidationOptions>, "maxTextureDataUrlBytes">): string;
293
316
  declare function validateRoomDefinition(room: RoomDefinition, overrides?: ValidationOptions): RoomDefinition;
294
317
  declare const geometry: {
295
318
  box: (width: number, height: number, depth: number) => GeometrySpec;
@@ -341,4 +364,4 @@ declare function placeFloor(id: string, itemId: string, x: number, z: number, op
341
364
  declare function placeWall(id: string, itemId: string, wall: WallName, offset: number, bottom: number, options?: Omit<WallPlacement, "mode" | "wall" | "offset" | "bottom">): RoomInstance;
342
365
  declare function place(id: string, itemId: string, position: Vec3, options?: Omit<FreePlacement, "mode" | "position">): RoomInstance;
343
366
 
344
- export { type AmbientLightSpec, type Atmosphere, type CameraView, type ColorValue, type CustomGeometrySpec, type DirectionalLightSpec, type Euler3, type FloorPlacement, type FreePlacement, type GeometrySpec, type GlassMaterialSpec, type HemisphereLightSpec, type ItemDefinition, type ItemNode, type ItemRefNode, type MaterialSide, type MaterialSpec, type MeshNode, type Placement, type PointLightNode, ROOM_FORMAT_VERSION, type RoomDefinition, type RoomInstance, type RoomLight, type RoomShell, type ShapePath, type StandardMaterialSpec, type TextureRef, type Transform, type UnlitMaterialSpec, type ValidationOptions, type Vec2, type Vec3, type WallName, type WallPlacement, ambientLight, defineItem, defineRoom, directionalLight, geometry, hemisphereLight, item, material, mesh, place, placeFloor, placeWall, pointLight, validateRoomDefinition };
367
+ export { type AmbientLightSpec, type Atmosphere, type CameraView, type ColorValue, type CustomGeometrySpec, type DirectionalLightSpec, type Euler3, type FloorPlacement, type FreePlacement, type GeometrySpec, type GlassMaterialSpec, type HemisphereLightSpec, type ItemDefinition, type ItemNode, type ItemRefNode, MAX_CEILING_GRID_LINES, MAX_ESTIMATED_GEOMETRY_VERTICES, MAX_EXPANDED_POINT_LIGHTS, MAX_GEOMETRY_DETAIL, MAX_GEOMETRY_POINTS, MAX_GEOMETRY_TESSELLATION_SEGMENTS, MAX_RENDERED_OBJECTS, MAX_TEXTURE_DATA_URL_BYTES, MIN_CEILING_GRID_SPACING, type MaterialSide, type MaterialSpec, type MeshNode, type Placement, type PointLightNode, ROOM_FORMAT_VERSION, type RoomDefinition, type RoomInstance, type RoomLight, type RoomShell, type ShapePath, type StandardMaterialSpec, type TextureRef, type Transform, type UnlitMaterialSpec, type ValidationOptions, type Vec2, type Vec3, type WallName, type WallPlacement, ambientLight, defineItem, defineRoom, directionalLight, geometry, hemisphereLight, isDataTextureUrl, item, material, mesh, place, placeFloor, placeWall, pointLight, validateRoomDefinition, validateTextureRef };
package/dist/index.js CHANGED
@@ -1,10 +1,31 @@
1
1
  // src/index.ts
2
2
  var ROOM_FORMAT_VERSION = 1;
3
+ var MAX_TEXTURE_DATA_URL_BYTES = 5 * 1024 * 1024;
4
+ var MAX_GEOMETRY_TESSELLATION_SEGMENTS = 256;
5
+ var MAX_GEOMETRY_DETAIL = 6;
6
+ var MAX_GEOMETRY_POINTS = 4096;
7
+ var MAX_ESTIMATED_GEOMETRY_VERTICES = 5e5;
8
+ var MIN_CEILING_GRID_SPACING = 0.05;
9
+ var MAX_CEILING_GRID_LINES = 256;
10
+ var MAX_RENDERED_OBJECTS = 1e4;
11
+ var MAX_EXPANDED_POINT_LIGHTS = 2e3;
3
12
  var defaultValidationOptions = {
4
13
  maxItems: 128,
14
+ maxItemNodes: 2048,
5
15
  maxInstances: 160,
6
16
  maxLights: 24,
7
- maxShellExtent: 64
17
+ maxShellExtent: 64,
18
+ maxCustomGeometryValues: 2e5,
19
+ maxShapePoints: 4096,
20
+ maxGeometryPoints: MAX_GEOMETRY_POINTS,
21
+ maxGeometryTessellationSegments: MAX_GEOMETRY_TESSELLATION_SEGMENTS,
22
+ maxGeometryDetail: MAX_GEOMETRY_DETAIL,
23
+ maxEstimatedGeometryVertices: MAX_ESTIMATED_GEOMETRY_VERTICES,
24
+ maxTextureDataUrlBytes: MAX_TEXTURE_DATA_URL_BYTES,
25
+ minCeilingGridSpacing: MIN_CEILING_GRID_SPACING,
26
+ maxCeilingGridLines: MAX_CEILING_GRID_LINES,
27
+ maxRenderedObjects: MAX_RENDERED_OBJECTS,
28
+ maxExpandedPointLights: MAX_EXPANDED_POINT_LIGHTS
8
29
  };
9
30
  function assertFiniteNumber(value, label) {
10
31
  if (!Number.isFinite(value)) {
@@ -12,20 +33,105 @@ function assertFiniteNumber(value, label) {
12
33
  }
13
34
  }
14
35
  function assertVec3(value, label) {
15
- if (!value) {
16
- return;
36
+ if (!Array.isArray(value) || value.length !== 3) {
37
+ throw new Error(`${label} must be a 3-number tuple`);
17
38
  }
18
39
  for (const [index, part] of value.entries()) {
19
40
  assertFiniteNumber(part, `${label}[${index}]`);
20
41
  }
21
42
  }
22
- function assertEuler(value, label) {
43
+ function assertOptionalVec3(value, label) {
44
+ if (!value) {
45
+ return;
46
+ }
23
47
  assertVec3(value, label);
24
48
  }
49
+ function assertEuler(value, label) {
50
+ assertOptionalVec3(value, label);
51
+ }
25
52
  function assertTransform(value, label) {
26
- assertVec3(value.position, `${label}.position`);
53
+ assertOptionalVec3(value.position, `${label}.position`);
27
54
  assertEuler(value.rotation, `${label}.rotation`);
28
- assertVec3(value.scale, `${label}.scale`);
55
+ assertOptionalVec3(value.scale, `${label}.scale`);
56
+ }
57
+ function assertMaxLength(value, max, label) {
58
+ if (value.length > max) {
59
+ throw new Error(`${label} exceeds the allowed length of ${max}`);
60
+ }
61
+ }
62
+ function assertBoundedInteger(value, label, max, min = 0) {
63
+ if (value === void 0) {
64
+ return;
65
+ }
66
+ if (!Number.isInteger(value) || value < min || value > max) {
67
+ throw new Error(`${label} must be an integer between ${min} and ${max}`);
68
+ }
69
+ }
70
+ function assertFiniteNumberArray(values, label) {
71
+ if (!Array.isArray(values)) {
72
+ throw new Error(`${label} must be an array`);
73
+ }
74
+ for (const [index, value] of values.entries()) {
75
+ assertFiniteNumber(value, `${label}[${index}]`);
76
+ }
77
+ }
78
+ function assertVec2(value, label) {
79
+ if (!Array.isArray(value) || value.length !== 2) {
80
+ throw new Error(`${label} must be a 2-number tuple`);
81
+ }
82
+ for (const [index, part] of value.entries()) {
83
+ assertFiniteNumber(part, `${label}[${index}]`);
84
+ }
85
+ }
86
+ function assertVec2Array(values, label, minLength, maxLength) {
87
+ if (!Array.isArray(values)) {
88
+ throw new Error(`${label} must be an array`);
89
+ }
90
+ if (values.length < minLength) {
91
+ throw new Error(`${label} must have at least ${minLength} points`);
92
+ }
93
+ assertMaxLength(values, maxLength, label);
94
+ for (const [index, point] of values.entries()) {
95
+ assertVec2(point, `${label}[${index}]`);
96
+ }
97
+ }
98
+ function assertVec3Array(values, label, minLength, maxLength) {
99
+ if (!Array.isArray(values)) {
100
+ throw new Error(`${label} must be an array`);
101
+ }
102
+ if (values.length < minLength) {
103
+ throw new Error(`${label} must have at least ${minLength} points`);
104
+ }
105
+ assertMaxLength(values, maxLength, label);
106
+ for (const [index, point] of values.entries()) {
107
+ assertVec3(point, `${label}[${index}]`);
108
+ }
109
+ }
110
+ function encodedByteLength(value) {
111
+ return new TextEncoder().encode(value).length;
112
+ }
113
+ function isDataTextureUrl(source) {
114
+ if (!source || source.trim() !== source) {
115
+ return false;
116
+ }
117
+ try {
118
+ return new URL(source).protocol === "data:";
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+ function validateTextureRef(source, label, options = {
124
+ maxTextureDataUrlBytes: MAX_TEXTURE_DATA_URL_BYTES
125
+ }) {
126
+ if (typeof source !== "string" || !isDataTextureUrl(source)) {
127
+ throw new Error(`${label} must be a data: URL`);
128
+ }
129
+ if (encodedByteLength(source) > options.maxTextureDataUrlBytes) {
130
+ throw new Error(
131
+ `${label} exceeds the allowed size of ${options.maxTextureDataUrlBytes} bytes`
132
+ );
133
+ }
134
+ return source;
29
135
  }
30
136
  function visitItem(itemId, items, visiting, visited) {
31
137
  if (visited.has(itemId)) {
@@ -47,93 +153,593 @@ function visitItem(itemId, items, visiting, visited) {
47
153
  visiting.delete(itemId);
48
154
  visited.add(itemId);
49
155
  }
50
- function validateGeometry(spec, label) {
156
+ function countShapePathPoints(path) {
157
+ return path.outline.length + (path.holes?.reduce((total, hole) => total + hole.length, 0) ?? 0);
158
+ }
159
+ function validateShapePath(path, label, options) {
160
+ if (countShapePathPoints(path) > options.maxShapePoints) {
161
+ throw new Error(`${label} exceeds the allowed point count`);
162
+ }
163
+ assertVec2Array(path.outline, `${label}.outline`, 3, options.maxShapePoints);
164
+ for (const [index, hole] of (path.holes ?? []).entries()) {
165
+ assertVec2Array(hole, `${label}.holes[${index}]`, 3, options.maxShapePoints);
166
+ }
167
+ }
168
+ function boundedSegments(value, label, options, defaultValue, min = 1) {
169
+ assertBoundedInteger(
170
+ value,
171
+ label,
172
+ options.maxGeometryTessellationSegments,
173
+ min
174
+ );
175
+ return value ?? defaultValue;
176
+ }
177
+ function boundedDetail(value, label, options) {
178
+ assertBoundedInteger(value, label, options.maxGeometryDetail);
179
+ return value ?? 0;
180
+ }
181
+ function assertEstimatedGeometryVertices(count, label, options) {
182
+ if (count > options.maxEstimatedGeometryVertices) {
183
+ throw new Error(
184
+ `${label} exceeds the estimated vertex budget of ${options.maxEstimatedGeometryVertices}`
185
+ );
186
+ }
187
+ }
188
+ function assertRenderedObjectCount(count, label, options) {
189
+ if (count > options.maxRenderedObjects) {
190
+ throw new Error(
191
+ `${label} exceeds the rendered object budget of ${options.maxRenderedObjects}`
192
+ );
193
+ }
194
+ }
195
+ function assertExpandedPointLightCount(count, label, options) {
196
+ if (count > options.maxExpandedPointLights) {
197
+ throw new Error(
198
+ `${label} exceeds the expanded point-light budget of ${options.maxExpandedPointLights}`
199
+ );
200
+ }
201
+ }
202
+ function addItemRenderCost(left, right) {
203
+ left.geometryVertices += right.geometryVertices;
204
+ left.objects += right.objects;
205
+ left.pointLights += right.pointLights;
206
+ }
207
+ function multiplyItemRenderCost(cost, multiplier) {
208
+ return {
209
+ geometryVertices: cost.geometryVertices * multiplier,
210
+ objects: cost.objects * multiplier,
211
+ pointLights: cost.pointLights * multiplier
212
+ };
213
+ }
214
+ function assertItemRenderCost(cost, label, options) {
215
+ assertEstimatedGeometryVertices(cost.geometryVertices, label, options);
216
+ assertRenderedObjectCount(cost.objects, label, options);
217
+ assertExpandedPointLightCount(cost.pointLights, label, options);
218
+ }
219
+ function validateCeilingGridBudget(room, options) {
220
+ const ceilingGrid = room.shell.ceilingGrid;
221
+ if (!ceilingGrid) {
222
+ return;
223
+ }
224
+ if (ceilingGrid.spacing < options.minCeilingGridSpacing) {
225
+ throw new Error(
226
+ `room.shell.ceilingGrid.spacing must be at least ${options.minCeilingGridSpacing}`
227
+ );
228
+ }
229
+ const estimatedLineCount = Math.ceil(room.shell.width / ceilingGrid.spacing) + Math.ceil(room.shell.depth / ceilingGrid.spacing);
230
+ if (estimatedLineCount > options.maxCeilingGridLines) {
231
+ throw new Error(
232
+ `room.shell.ceilingGrid exceeds the ${options.maxCeilingGridLines} line budget`
233
+ );
234
+ }
235
+ }
236
+ function estimateExpandedItemRenderCost(itemId, items, directRenderCostByItem, cache, visiting, options) {
237
+ const cached = cache.get(itemId);
238
+ if (cached !== void 0) {
239
+ return cached;
240
+ }
241
+ if (visiting.has(itemId)) {
242
+ throw new Error(`Recursive item definition detected at "${itemId}"`);
243
+ }
244
+ const item2 = items[itemId];
245
+ if (!item2) {
246
+ throw new Error(`Missing item definition "${itemId}"`);
247
+ }
248
+ visiting.add(itemId);
249
+ const total = {
250
+ geometryVertices: 0,
251
+ objects: 1,
252
+ pointLights: 0,
253
+ ...directRenderCostByItem.get(itemId) ?? {}
254
+ };
255
+ for (const node of item2.nodes) {
256
+ if (node.kind !== "item") {
257
+ continue;
258
+ }
259
+ addItemRenderCost(
260
+ total,
261
+ estimateExpandedItemRenderCost(
262
+ node.itemId,
263
+ items,
264
+ directRenderCostByItem,
265
+ cache,
266
+ visiting,
267
+ options
268
+ )
269
+ );
270
+ assertItemRenderCost(
271
+ total,
272
+ `room.items.${itemId} expanded render cost`,
273
+ options
274
+ );
275
+ }
276
+ visiting.delete(itemId);
277
+ cache.set(itemId, total);
278
+ return total;
279
+ }
280
+ function subdividedTriangleVertexCount(baseFaces, detail) {
281
+ return baseFaces * 3 * 4 ** detail;
282
+ }
283
+ function validateGeometry(spec, label, options) {
284
+ let estimatedVertices = 0;
51
285
  switch (spec.type) {
52
286
  case "box":
53
- case "rounded-box":
54
287
  assertFiniteNumber(spec.width, `${label}.width`);
55
288
  assertFiniteNumber(spec.height, `${label}.height`);
56
289
  assertFiniteNumber(spec.depth, `${label}.depth`);
290
+ estimatedVertices = 24;
57
291
  break;
58
- case "capsule":
292
+ case "rounded-box": {
293
+ assertFiniteNumber(spec.width, `${label}.width`);
294
+ assertFiniteNumber(spec.height, `${label}.height`);
295
+ assertFiniteNumber(spec.depth, `${label}.depth`);
296
+ if (spec.radius !== void 0) {
297
+ assertFiniteNumber(spec.radius, `${label}.radius`);
298
+ }
299
+ const segments = boundedSegments(
300
+ spec.segments,
301
+ `${label}.segments`,
302
+ options,
303
+ 4
304
+ );
305
+ estimatedVertices = 6 * (segments + 1) ** 2;
306
+ break;
307
+ }
308
+ case "capsule": {
59
309
  assertFiniteNumber(spec.radius, `${label}.radius`);
60
310
  assertFiniteNumber(spec.length, `${label}.length`);
311
+ const capSegments = boundedSegments(
312
+ spec.capSegments,
313
+ `${label}.capSegments`,
314
+ options,
315
+ 8
316
+ );
317
+ const radialSegments = boundedSegments(
318
+ spec.radialSegments,
319
+ `${label}.radialSegments`,
320
+ options,
321
+ 12
322
+ );
323
+ estimatedVertices = (radialSegments + 1) * (capSegments * 2 + 2);
61
324
  break;
62
- case "circle":
325
+ }
326
+ case "circle": {
63
327
  assertFiniteNumber(spec.radius, `${label}.radius`);
328
+ const segments = boundedSegments(
329
+ spec.segments,
330
+ `${label}.segments`,
331
+ options,
332
+ 32,
333
+ 3
334
+ );
335
+ if (spec.thetaStart !== void 0) {
336
+ assertFiniteNumber(spec.thetaStart, `${label}.thetaStart`);
337
+ }
338
+ if (spec.thetaLength !== void 0) {
339
+ assertFiniteNumber(spec.thetaLength, `${label}.thetaLength`);
340
+ }
341
+ estimatedVertices = segments + 2;
64
342
  break;
65
- case "plane":
343
+ }
344
+ case "plane": {
66
345
  assertFiniteNumber(spec.width, `${label}.width`);
67
346
  assertFiniteNumber(spec.height, `${label}.height`);
347
+ const widthSegments = boundedSegments(
348
+ spec.widthSegments,
349
+ `${label}.widthSegments`,
350
+ options,
351
+ 1
352
+ );
353
+ const heightSegments = boundedSegments(
354
+ spec.heightSegments,
355
+ `${label}.heightSegments`,
356
+ options,
357
+ 1
358
+ );
359
+ estimatedVertices = (widthSegments + 1) * (heightSegments + 1);
68
360
  break;
69
- case "sphere":
361
+ }
362
+ case "sphere": {
70
363
  assertFiniteNumber(spec.radius, `${label}.radius`);
364
+ const widthSegments = boundedSegments(
365
+ spec.widthSegments,
366
+ `${label}.widthSegments`,
367
+ options,
368
+ 32,
369
+ 3
370
+ );
371
+ const heightSegments = boundedSegments(
372
+ spec.heightSegments,
373
+ `${label}.heightSegments`,
374
+ options,
375
+ 16,
376
+ 2
377
+ );
378
+ if (spec.phiStart !== void 0) {
379
+ assertFiniteNumber(spec.phiStart, `${label}.phiStart`);
380
+ }
381
+ if (spec.phiLength !== void 0) {
382
+ assertFiniteNumber(spec.phiLength, `${label}.phiLength`);
383
+ }
384
+ if (spec.thetaStart !== void 0) {
385
+ assertFiniteNumber(spec.thetaStart, `${label}.thetaStart`);
386
+ }
387
+ if (spec.thetaLength !== void 0) {
388
+ assertFiniteNumber(spec.thetaLength, `${label}.thetaLength`);
389
+ }
390
+ estimatedVertices = (widthSegments + 1) * (heightSegments + 1);
71
391
  break;
72
- case "cylinder":
392
+ }
393
+ case "cylinder": {
73
394
  assertFiniteNumber(spec.radiusTop, `${label}.radiusTop`);
74
395
  assertFiniteNumber(spec.radiusBottom, `${label}.radiusBottom`);
75
396
  assertFiniteNumber(spec.height, `${label}.height`);
397
+ const radialSegments = boundedSegments(
398
+ spec.radialSegments,
399
+ `${label}.radialSegments`,
400
+ options,
401
+ 32,
402
+ 3
403
+ );
404
+ const heightSegments = boundedSegments(
405
+ spec.heightSegments,
406
+ `${label}.heightSegments`,
407
+ options,
408
+ 1
409
+ );
410
+ estimatedVertices = (radialSegments + 1) * (heightSegments + 3);
76
411
  break;
77
- case "cone":
412
+ }
413
+ case "cone": {
78
414
  assertFiniteNumber(spec.radius, `${label}.radius`);
79
415
  assertFiniteNumber(spec.height, `${label}.height`);
416
+ const radialSegments = boundedSegments(
417
+ spec.radialSegments,
418
+ `${label}.radialSegments`,
419
+ options,
420
+ 32,
421
+ 3
422
+ );
423
+ const heightSegments = boundedSegments(
424
+ spec.heightSegments,
425
+ `${label}.heightSegments`,
426
+ options,
427
+ 1
428
+ );
429
+ estimatedVertices = (radialSegments + 1) * (heightSegments + 3);
80
430
  break;
81
- case "ring":
431
+ }
432
+ case "ring": {
82
433
  assertFiniteNumber(spec.innerRadius, `${label}.innerRadius`);
83
434
  assertFiniteNumber(spec.outerRadius, `${label}.outerRadius`);
435
+ const thetaSegments = boundedSegments(
436
+ spec.thetaSegments,
437
+ `${label}.thetaSegments`,
438
+ options,
439
+ 32,
440
+ 3
441
+ );
442
+ const phiSegments = boundedSegments(
443
+ spec.phiSegments,
444
+ `${label}.phiSegments`,
445
+ options,
446
+ 1
447
+ );
448
+ if (spec.thetaStart !== void 0) {
449
+ assertFiniteNumber(spec.thetaStart, `${label}.thetaStart`);
450
+ }
451
+ if (spec.thetaLength !== void 0) {
452
+ assertFiniteNumber(spec.thetaLength, `${label}.thetaLength`);
453
+ }
454
+ estimatedVertices = (thetaSegments + 1) * (phiSegments + 1);
455
+ break;
456
+ }
457
+ case "torus": {
458
+ assertFiniteNumber(spec.radius, `${label}.radius`);
459
+ assertFiniteNumber(spec.tube, `${label}.tube`);
460
+ const radialSegments = boundedSegments(
461
+ spec.radialSegments,
462
+ `${label}.radialSegments`,
463
+ options,
464
+ 12,
465
+ 3
466
+ );
467
+ const tubularSegments = boundedSegments(
468
+ spec.tubularSegments,
469
+ `${label}.tubularSegments`,
470
+ options,
471
+ 48,
472
+ 3
473
+ );
474
+ if (spec.arc !== void 0) {
475
+ assertFiniteNumber(spec.arc, `${label}.arc`);
476
+ }
477
+ estimatedVertices = (radialSegments + 1) * (tubularSegments + 1);
84
478
  break;
85
- case "torus":
86
- case "torus-knot":
479
+ }
480
+ case "torus-knot": {
87
481
  assertFiniteNumber(spec.radius, `${label}.radius`);
88
482
  assertFiniteNumber(spec.tube, `${label}.tube`);
483
+ const tubularSegments = boundedSegments(
484
+ spec.tubularSegments,
485
+ `${label}.tubularSegments`,
486
+ options,
487
+ 64,
488
+ 3
489
+ );
490
+ const radialSegments = boundedSegments(
491
+ spec.radialSegments,
492
+ `${label}.radialSegments`,
493
+ options,
494
+ 8,
495
+ 3
496
+ );
497
+ assertBoundedInteger(
498
+ spec.p,
499
+ `${label}.p`,
500
+ options.maxGeometryTessellationSegments,
501
+ 1
502
+ );
503
+ assertBoundedInteger(
504
+ spec.q,
505
+ `${label}.q`,
506
+ options.maxGeometryTessellationSegments,
507
+ 1
508
+ );
509
+ estimatedVertices = (radialSegments + 1) * (tubularSegments + 1);
89
510
  break;
90
- case "tetrahedron":
91
- case "octahedron":
92
- case "icosahedron":
93
- case "dodecahedron":
511
+ }
512
+ case "tetrahedron": {
94
513
  if (spec.radius !== void 0) {
95
514
  assertFiniteNumber(spec.radius, `${label}.radius`);
96
515
  }
516
+ const detail = boundedDetail(spec.detail, `${label}.detail`, options);
517
+ estimatedVertices = subdividedTriangleVertexCount(4, detail);
97
518
  break;
98
- case "polyhedron":
519
+ }
520
+ case "octahedron": {
521
+ if (spec.radius !== void 0) {
522
+ assertFiniteNumber(spec.radius, `${label}.radius`);
523
+ }
524
+ const detail = boundedDetail(spec.detail, `${label}.detail`, options);
525
+ estimatedVertices = subdividedTriangleVertexCount(8, detail);
526
+ break;
527
+ }
528
+ case "icosahedron": {
529
+ if (spec.radius !== void 0) {
530
+ assertFiniteNumber(spec.radius, `${label}.radius`);
531
+ }
532
+ const detail = boundedDetail(spec.detail, `${label}.detail`, options);
533
+ estimatedVertices = subdividedTriangleVertexCount(20, detail);
534
+ break;
535
+ }
536
+ case "dodecahedron": {
537
+ if (spec.radius !== void 0) {
538
+ assertFiniteNumber(spec.radius, `${label}.radius`);
539
+ }
540
+ const detail = boundedDetail(spec.detail, `${label}.detail`, options);
541
+ estimatedVertices = subdividedTriangleVertexCount(36, detail);
542
+ break;
543
+ }
544
+ case "polyhedron": {
545
+ assertMaxLength(
546
+ spec.vertices,
547
+ options.maxCustomGeometryValues,
548
+ `${label}.vertices`
549
+ );
550
+ assertMaxLength(
551
+ spec.indices,
552
+ options.maxCustomGeometryValues,
553
+ `${label}.indices`
554
+ );
555
+ assertFiniteNumberArray(spec.vertices, `${label}.vertices`);
556
+ assertFiniteNumberArray(spec.indices, `${label}.indices`);
99
557
  if (spec.vertices.length % 3 !== 0) {
100
558
  throw new Error(`${label}.vertices must be a multiple of 3`);
101
559
  }
560
+ if (spec.indices.length % 3 !== 0) {
561
+ throw new Error(`${label}.indices must be a multiple of 3`);
562
+ }
563
+ if (spec.radius !== void 0) {
564
+ assertFiniteNumber(spec.radius, `${label}.radius`);
565
+ }
566
+ const detail = boundedDetail(spec.detail, `${label}.detail`, options);
567
+ estimatedVertices = (spec.indices.length || spec.vertices.length) * 4 ** detail;
102
568
  break;
569
+ }
103
570
  case "shape":
104
- if (spec.path.outline.length < 3) {
105
- throw new Error(`${label}.path.outline must have at least 3 points`);
571
+ validateShapePath(spec.path, `${label}.path`, options);
572
+ if (spec.depth !== void 0) {
573
+ assertFiniteNumber(spec.depth, `${label}.depth`);
106
574
  }
575
+ estimatedVertices = countShapePathPoints(spec.path) * (spec.depth ? 4 : 2);
107
576
  break;
108
577
  case "extrude":
109
- if (spec.path.outline.length < 3) {
110
- throw new Error(`${label}.path.outline must have at least 3 points`);
111
- }
578
+ validateShapePath(spec.path, `${label}.path`, options);
112
579
  assertFiniteNumber(spec.depth, `${label}.depth`);
580
+ const steps = boundedSegments(spec.steps, `${label}.steps`, options, 1);
581
+ const bevelSegments = boundedSegments(
582
+ spec.bevelSegments,
583
+ `${label}.bevelSegments`,
584
+ options,
585
+ 3
586
+ );
587
+ if (spec.bevelThickness !== void 0) {
588
+ assertFiniteNumber(spec.bevelThickness, `${label}.bevelThickness`);
589
+ }
590
+ if (spec.bevelSize !== void 0) {
591
+ assertFiniteNumber(spec.bevelSize, `${label}.bevelSize`);
592
+ }
593
+ if (spec.bevelOffset !== void 0) {
594
+ assertFiniteNumber(spec.bevelOffset, `${label}.bevelOffset`);
595
+ }
596
+ estimatedVertices = countShapePathPoints(spec.path) * (steps + bevelSegments + 2) * 2;
113
597
  break;
114
- case "lathe":
115
- if (spec.points.length < 2) {
116
- throw new Error(`${label}.points must have at least 2 points`);
598
+ case "lathe": {
599
+ assertVec2Array(
600
+ spec.points,
601
+ `${label}.points`,
602
+ 2,
603
+ options.maxGeometryPoints
604
+ );
605
+ const segments = boundedSegments(
606
+ spec.segments,
607
+ `${label}.segments`,
608
+ options,
609
+ 12,
610
+ 3
611
+ );
612
+ if (spec.phiStart !== void 0) {
613
+ assertFiniteNumber(spec.phiStart, `${label}.phiStart`);
117
614
  }
615
+ if (spec.phiLength !== void 0) {
616
+ assertFiniteNumber(spec.phiLength, `${label}.phiLength`);
617
+ }
618
+ estimatedVertices = spec.points.length * (segments + 1);
118
619
  break;
119
- case "tube":
120
- if (spec.points.length < 2) {
121
- throw new Error(`${label}.points must have at least 2 points`);
620
+ }
621
+ case "tube": {
622
+ assertVec3Array(
623
+ spec.points,
624
+ `${label}.points`,
625
+ 2,
626
+ options.maxGeometryPoints
627
+ );
628
+ const tubularSegments = boundedSegments(
629
+ spec.tubularSegments,
630
+ `${label}.tubularSegments`,
631
+ options,
632
+ 64
633
+ );
634
+ if (spec.radius !== void 0) {
635
+ assertFiniteNumber(spec.radius, `${label}.radius`);
122
636
  }
637
+ const radialSegments = boundedSegments(
638
+ spec.radialSegments,
639
+ `${label}.radialSegments`,
640
+ options,
641
+ 8,
642
+ 3
643
+ );
644
+ estimatedVertices = (tubularSegments + 1) * (radialSegments + 1);
123
645
  break;
646
+ }
124
647
  case "custom":
648
+ assertMaxLength(
649
+ spec.positions,
650
+ options.maxCustomGeometryValues,
651
+ `${label}.positions`
652
+ );
653
+ if (spec.indices) {
654
+ assertMaxLength(
655
+ spec.indices,
656
+ options.maxCustomGeometryValues,
657
+ `${label}.indices`
658
+ );
659
+ }
660
+ if (spec.normals) {
661
+ assertMaxLength(
662
+ spec.normals,
663
+ options.maxCustomGeometryValues,
664
+ `${label}.normals`
665
+ );
666
+ }
667
+ if (spec.uvs) {
668
+ assertMaxLength(
669
+ spec.uvs,
670
+ options.maxCustomGeometryValues,
671
+ `${label}.uvs`
672
+ );
673
+ }
125
674
  if (spec.positions.length === 0 || spec.positions.length % 3 !== 0) {
126
675
  throw new Error(`${label}.positions must be a non-empty multiple of 3`);
127
676
  }
677
+ assertFiniteNumberArray(spec.positions, `${label}.positions`);
128
678
  if (spec.indices && spec.indices.length % 3 !== 0) {
129
679
  throw new Error(`${label}.indices must be a multiple of 3`);
130
680
  }
681
+ if (spec.indices) {
682
+ assertFiniteNumberArray(spec.indices, `${label}.indices`);
683
+ }
131
684
  if (spec.normals && spec.normals.length !== spec.positions.length) {
132
685
  throw new Error(`${label}.normals must match positions length`);
133
686
  }
687
+ if (spec.normals) {
688
+ assertFiniteNumberArray(spec.normals, `${label}.normals`);
689
+ }
134
690
  if (spec.uvs && spec.uvs.length % 2 !== 0) {
135
691
  throw new Error(`${label}.uvs must be a multiple of 2`);
136
692
  }
693
+ if (spec.uvs) {
694
+ assertFiniteNumberArray(spec.uvs, `${label}.uvs`);
695
+ }
696
+ estimatedVertices = spec.positions.length / 3;
697
+ break;
698
+ default: {
699
+ const exhaustiveCheck = spec;
700
+ return exhaustiveCheck;
701
+ }
702
+ }
703
+ assertEstimatedGeometryVertices(estimatedVertices, label, options);
704
+ return estimatedVertices;
705
+ }
706
+ function validateMaterial(spec, label, options) {
707
+ switch (spec.type) {
708
+ case "standard":
709
+ if (spec.roughness !== void 0) {
710
+ assertFiniteNumber(spec.roughness, `${label}.roughness`);
711
+ }
712
+ if (spec.metalness !== void 0) {
713
+ assertFiniteNumber(spec.metalness, `${label}.metalness`);
714
+ }
715
+ if (spec.emissiveIntensity !== void 0) {
716
+ assertFiniteNumber(spec.emissiveIntensity, `${label}.emissiveIntensity`);
717
+ }
718
+ if (spec.opacity !== void 0) {
719
+ assertFiniteNumber(spec.opacity, `${label}.opacity`);
720
+ }
721
+ if (spec.map !== void 0) {
722
+ validateTextureRef(spec.map, `${label}.map`, options);
723
+ }
724
+ break;
725
+ case "unlit":
726
+ if (spec.opacity !== void 0) {
727
+ assertFiniteNumber(spec.opacity, `${label}.opacity`);
728
+ }
729
+ if (spec.map !== void 0) {
730
+ validateTextureRef(spec.map, `${label}.map`, options);
731
+ }
732
+ break;
733
+ case "glass":
734
+ if (spec.roughness !== void 0) {
735
+ assertFiniteNumber(spec.roughness, `${label}.roughness`);
736
+ }
737
+ if (spec.opacity !== void 0) {
738
+ assertFiniteNumber(spec.opacity, `${label}.opacity`);
739
+ }
740
+ if (spec.transmission !== void 0) {
741
+ assertFiniteNumber(spec.transmission, `${label}.transmission`);
742
+ }
137
743
  break;
138
744
  default: {
139
745
  const exhaustiveCheck = spec;
@@ -154,9 +760,38 @@ function validateRoomDefinition(room, overrides = {}) {
154
760
  assertFiniteNumber(room.shell.width, "room.shell.width");
155
761
  assertFiniteNumber(room.shell.depth, "room.shell.depth");
156
762
  assertFiniteNumber(room.shell.height, "room.shell.height");
763
+ validateMaterial(
764
+ room.shell.floorMaterial,
765
+ "room.shell.floorMaterial",
766
+ options
767
+ );
768
+ validateMaterial(room.shell.wallMaterial, "room.shell.wallMaterial", options);
769
+ validateMaterial(
770
+ room.shell.ceilingMaterial,
771
+ "room.shell.ceilingMaterial",
772
+ options
773
+ );
774
+ if (room.shell.ceilingGrid) {
775
+ assertFiniteNumber(
776
+ room.shell.ceilingGrid.spacing,
777
+ "room.shell.ceilingGrid.spacing"
778
+ );
779
+ if (room.shell.ceilingGrid.thickness !== void 0) {
780
+ assertFiniteNumber(
781
+ room.shell.ceilingGrid.thickness,
782
+ "room.shell.ceilingGrid.thickness"
783
+ );
784
+ }
785
+ validateMaterial(
786
+ room.shell.ceilingGrid.material,
787
+ "room.shell.ceilingGrid.material",
788
+ options
789
+ );
790
+ }
157
791
  if (room.shell.width <= 0 || room.shell.depth <= 0 || room.shell.height <= 0 || room.shell.width > options.maxShellExtent || room.shell.depth > options.maxShellExtent || room.shell.height > options.maxShellExtent) {
158
792
  throw new Error("Room shell dimensions are outside allowed bounds");
159
793
  }
794
+ validateCeilingGridBudget(room, options);
160
795
  const itemEntries = Object.entries(room.items);
161
796
  if (itemEntries.length > options.maxItems) {
162
797
  throw new Error(
@@ -174,16 +809,39 @@ function validateRoomDefinition(room, overrides = {}) {
174
809
  if (!room.cameras[room.defaultCamera]) {
175
810
  throw new Error(`Default camera "${room.defaultCamera}" does not exist`);
176
811
  }
812
+ const directRenderCostByItem = /* @__PURE__ */ new Map();
177
813
  for (const [itemId, item2] of itemEntries) {
178
814
  if (item2.id !== itemId) {
179
815
  throw new Error(`Item key "${itemId}" must match item.id "${item2.id}"`);
180
816
  }
817
+ if (item2.nodes.length > options.maxItemNodes) {
818
+ throw new Error(
819
+ `Item "${itemId}" has too many nodes (${item2.nodes.length})`
820
+ );
821
+ }
822
+ const itemDirectRenderCost = {
823
+ geometryVertices: 0,
824
+ objects: 0,
825
+ pointLights: 0
826
+ };
181
827
  for (const [index, node] of item2.nodes.entries()) {
182
828
  assertTransform(node, `room.items.${itemId}.nodes[${index}]`);
183
829
  if (node.kind === "mesh") {
184
- validateGeometry(
830
+ itemDirectRenderCost.geometryVertices += validateGeometry(
185
831
  node.geometry,
186
- `room.items.${itemId}.nodes[${index}].geometry`
832
+ `room.items.${itemId}.nodes[${index}].geometry`,
833
+ options
834
+ );
835
+ itemDirectRenderCost.objects += 1;
836
+ assertItemRenderCost(
837
+ itemDirectRenderCost,
838
+ `room.items.${itemId}`,
839
+ options
840
+ );
841
+ validateMaterial(
842
+ node.material,
843
+ `room.items.${itemId}.nodes[${index}].material`,
844
+ options
187
845
  );
188
846
  }
189
847
  if (node.kind === "item" && !room.items[node.itemId]) {
@@ -191,14 +849,53 @@ function validateRoomDefinition(room, overrides = {}) {
191
849
  `Item "${itemId}" references missing child item "${node.itemId}"`
192
850
  );
193
851
  }
852
+ if (node.kind === "item") {
853
+ itemDirectRenderCost.objects += 1;
854
+ assertItemRenderCost(
855
+ itemDirectRenderCost,
856
+ `room.items.${itemId}`,
857
+ options
858
+ );
859
+ }
860
+ if (node.kind === "point-light") {
861
+ itemDirectRenderCost.objects += 1;
862
+ itemDirectRenderCost.pointLights += 1;
863
+ assertItemRenderCost(
864
+ itemDirectRenderCost,
865
+ `room.items.${itemId}`,
866
+ options
867
+ );
868
+ assertFiniteNumber(
869
+ node.intensity,
870
+ `room.items.${itemId}.nodes[${index}].intensity`
871
+ );
872
+ if (node.distance !== void 0) {
873
+ assertFiniteNumber(
874
+ node.distance,
875
+ `room.items.${itemId}.nodes[${index}].distance`
876
+ );
877
+ }
878
+ if (node.decay !== void 0) {
879
+ assertFiniteNumber(
880
+ node.decay,
881
+ `room.items.${itemId}.nodes[${index}].decay`
882
+ );
883
+ }
884
+ }
194
885
  }
886
+ directRenderCostByItem.set(itemId, itemDirectRenderCost);
195
887
  }
888
+ const instanceCountsByItem = /* @__PURE__ */ new Map();
196
889
  for (const instance of room.instances) {
197
890
  if (!room.items[instance.itemId]) {
198
891
  throw new Error(
199
892
  `Instance "${instance.id}" references missing item "${instance.itemId}"`
200
893
  );
201
894
  }
895
+ instanceCountsByItem.set(
896
+ instance.itemId,
897
+ (instanceCountsByItem.get(instance.itemId) ?? 0) + 1
898
+ );
202
899
  if (instance.placement.mode === "free") {
203
900
  assertVec3(
204
901
  instance.placement.position,
@@ -208,7 +905,7 @@ function validateRoomDefinition(room, overrides = {}) {
208
905
  instance.placement.rotation,
209
906
  `room.instances.${instance.id}.placement.rotation`
210
907
  );
211
- assertVec3(
908
+ assertOptionalVec3(
212
909
  instance.placement.scale,
213
910
  `room.instances.${instance.id}.placement.scale`
214
911
  );
@@ -229,7 +926,7 @@ function validateRoomDefinition(room, overrides = {}) {
229
926
  `room.instances.${instance.id}.placement.rotationY`
230
927
  );
231
928
  }
232
- assertVec3(
929
+ assertOptionalVec3(
233
930
  instance.placement.scale,
234
931
  `room.instances.${instance.id}.placement.scale`
235
932
  );
@@ -249,11 +946,38 @@ function validateRoomDefinition(room, overrides = {}) {
249
946
  `room.instances.${instance.id}.placement.rotationY`
250
947
  );
251
948
  }
252
- assertVec3(
949
+ assertOptionalVec3(
253
950
  instance.placement.scale,
254
951
  `room.instances.${instance.id}.placement.scale`
255
952
  );
256
953
  }
954
+ const expandedRenderCostByItem = /* @__PURE__ */ new Map();
955
+ const totalInstancedRenderCost = {
956
+ geometryVertices: 0,
957
+ objects: 0,
958
+ pointLights: 0
959
+ };
960
+ for (const [itemId, instanceCount] of instanceCountsByItem.entries()) {
961
+ addItemRenderCost(
962
+ totalInstancedRenderCost,
963
+ multiplyItemRenderCost(
964
+ estimateExpandedItemRenderCost(
965
+ itemId,
966
+ room.items,
967
+ directRenderCostByItem,
968
+ expandedRenderCostByItem,
969
+ /* @__PURE__ */ new Set(),
970
+ options
971
+ ),
972
+ instanceCount
973
+ )
974
+ );
975
+ assertItemRenderCost(
976
+ totalInstancedRenderCost,
977
+ "room instanced render cost",
978
+ options
979
+ );
980
+ }
257
981
  for (const camera of Object.values(room.cameras)) {
258
982
  assertVec3(camera.position, "room.cameras.position");
259
983
  assertVec3(camera.target, "room.cameras.target");
@@ -261,6 +985,13 @@ function validateRoomDefinition(room, overrides = {}) {
261
985
  assertFiniteNumber(camera.fov, "room.cameras.fov");
262
986
  }
263
987
  }
988
+ for (const [index, light] of (room.lights ?? []).entries()) {
989
+ assertFiniteNumber(light.intensity, `room.lights[${index}].intensity`);
990
+ if (light.kind === "directional-light") {
991
+ assertVec3(light.position, `room.lights[${index}].position`);
992
+ assertOptionalVec3(light.target, `room.lights[${index}].target`);
993
+ }
994
+ }
264
995
  const visited = /* @__PURE__ */ new Set();
265
996
  for (const itemId of Object.keys(room.items)) {
266
997
  visitItem(itemId, room.items, /* @__PURE__ */ new Set(), visited);
@@ -509,6 +1240,15 @@ function place(id, itemId, position, options = {}) {
509
1240
  };
510
1241
  }
511
1242
  export {
1243
+ MAX_CEILING_GRID_LINES,
1244
+ MAX_ESTIMATED_GEOMETRY_VERTICES,
1245
+ MAX_EXPANDED_POINT_LIGHTS,
1246
+ MAX_GEOMETRY_DETAIL,
1247
+ MAX_GEOMETRY_POINTS,
1248
+ MAX_GEOMETRY_TESSELLATION_SEGMENTS,
1249
+ MAX_RENDERED_OBJECTS,
1250
+ MAX_TEXTURE_DATA_URL_BYTES,
1251
+ MIN_CEILING_GRID_SPACING,
512
1252
  ROOM_FORMAT_VERSION,
513
1253
  ambientLight,
514
1254
  defineItem,
@@ -516,6 +1256,7 @@ export {
516
1256
  directionalLight,
517
1257
  geometry,
518
1258
  hemisphereLight,
1259
+ isDataTextureUrl,
519
1260
  item,
520
1261
  material,
521
1262
  mesh,
@@ -523,5 +1264,6 @@ export {
523
1264
  placeFloor,
524
1265
  placeWall,
525
1266
  pointLight,
526
- validateRoomDefinition
1267
+ validateRoomDefinition,
1268
+ validateTextureRef
527
1269
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rooms.sh/sdk",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",
@@ -17,16 +17,16 @@
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
20
- "scripts": {
21
- "build": "tsup src/index.ts --format esm --dts --clean",
22
- "lint": "eslint",
23
- "format": "prettier --write \"**/*.{ts,tsx}\"",
24
- "typecheck": "tsc --noEmit"
25
- },
26
20
  "devDependencies": {
27
21
  "@types/node": "^25.1.0",
28
22
  "tsup": "^8.5.1",
29
23
  "typescript": "^5.9.3",
30
24
  "vitest": "^3.2.4"
25
+ },
26
+ "scripts": {
27
+ "build": "tsup src/index.ts --format esm --dts --clean",
28
+ "lint": "eslint",
29
+ "format": "prettier --write \"**/*.{ts,tsx}\"",
30
+ "typecheck": "tsc --noEmit"
31
31
  }
32
- }
32
+ }