@rooms.sh/sdk 0.0.2 → 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 +24 -1
- package/dist/index.js +781 -39
- package/package.json +8 -8
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
|
-
|
|
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
|
|
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
|
-
|
|
53
|
+
assertOptionalVec3(value.position, `${label}.position`);
|
|
27
54
|
assertEuler(value.rotation, `${label}.rotation`);
|
|
28
|
-
|
|
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
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
case "
|
|
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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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
|
+
}
|