@rig-c/three 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -40,15 +40,17 @@ requestAnimationFrame(frame);
40
40
  ```
41
41
 
42
42
  Use the model document and image pages from `rigc build`, unchanged. Tested with
43
- Three.js **0.186.1** and play **1.10.0**. The API is 0.x. See [`packages/three` in the repository](https://github.com/rig-c/adapters/tree/main/packages/three) for source and parity tests.
43
+ Three.js **0.186.1** and play **1.20.0** (`@rig-c/play >=1.20.0 <2`). The API is 0.x. See [`packages/three` in the repository](https://github.com/rig-c/adapters/tree/main/packages/three) for source and parity tests.
44
44
 
45
45
  ## Live object
46
46
 
47
47
  `play(name, options)`, `seek(seconds)`, `advance(dt)` and `update(dt)` forward to
48
- an independent `RigPlayer`; deltas are seconds, and the player's `timescale`
48
+ an independent `RigPlayer` and consume its `liveFrame()` lease synchronously
49
+ (`liveFrameChecks`, `poser`, `renderFrame(frame)`); deltas are seconds, and the player's `timescale`
49
50
  (alias `timeScale`) applies exactly once. The app owns the loop. `timescale = 0`
50
- holds the drawn frame and uploads no geometry. `drawnFrame()` is the exact record
51
- passed to the native renderer; `posedFrame(filter)`, `bone(name)` and `bounds()`
51
+ holds the drawn frame and uploads no geometry. `draw()` returns a detached
52
+ `PosedFrame`; `drawnFrame()` returns a detached snapshot of the last draw,
53
+ including after direct mutations of `rig.player`; `posedFrame(filter)`, `bone(name)` and `bounds()`
52
54
  use play's readers. World bounds are in rig-local Y-up space, before this Group's
53
55
  transform. `resourceCounts()` reports owned meshes, geometries, page materials,
54
56
  clip textures and retained/shared pages. `destroy()` is idempotent and frees
@@ -64,7 +66,7 @@ both straight and premultiplied pixels: conflicting `pma` records are refused.
64
66
  ## Native drawing and scene placement
65
67
 
66
68
  `ThreeRenderer(root, assets, webglRenderer)` is available when the app owns the
67
- player. Call `render(player.posedFrame())`, then render the scene normally.
69
+ player. Call `render(player.liveFrame())` (or `render(player.posedFrame())`), then render the scene normally.
68
70
  No internal canvas, offscreen bitmap of the rig, or fallback renderer draws it.
69
71
  Positions and page UVs come directly from the record; the full atlas page is
70
72
  sampled, so packed rotation needs no separate rule. The root stays Y-up and may
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as three from 'three';
2
2
  import { Group, WebGLRenderer, Texture, Mesh, BufferGeometry, ShaderMaterial, Camera } from 'three';
3
- import { ModelAssets, PosedFrame, RigPlayer, SlotFilter, PlayOptions, BoneWorld, PoseBounds, PosedSlot } from '@rig-c/play';
3
+ import { ModelAssets, PosedFrame, LiveFrame, RigPlayer, SlotFilter, PlayOptions, BoneWorld, PoseBounds, PosedSlot } from '@rig-c/play';
4
4
 
5
5
  declare const getLivePageTextureCount: () => number;
6
6
  declare const getPageRefCount: (image: HTMLImageElement) => number;
@@ -31,14 +31,20 @@ declare class ThreeRenderer {
31
31
  private readonly pool;
32
32
  private readonly materials;
33
33
  private readonly retained;
34
+ private readonly liveViews;
35
+ private readonly retainedRuntimeFrame;
36
+ private accepted;
37
+ private hasRetained;
34
38
  constructor(root: Group, assets: ModelAssets, host: WebGLRenderer);
39
+ get drawnFrame(): PosedFrame | null;
35
40
  get geometryUploadCount(): number;
36
41
  get isDisposed(): boolean;
37
42
  resourceCounts(): ThreeRigResourceCounts;
38
43
  getPageTexture(name: string): Texture | undefined;
39
44
  private prepare;
40
45
  private material;
41
- render(frame: PosedFrame): void;
46
+ render(frame: PosedFrame | LiveFrame): void;
47
+ private renderOrder;
42
48
  dispose(): void;
43
49
  }
44
50
  /** Read native float32 positions through the application's world and camera mapping. */
@@ -49,6 +55,8 @@ interface ThreeRigOptions {
49
55
  slots?: SlotFilter;
50
56
  timescale?: number;
51
57
  timeScale?: number;
58
+ liveFrameChecks?: boolean;
59
+ poser?: 'auto' | 'oracle';
52
60
  }
53
61
  /** A flat native Group. Advance from the app's loop using seconds, without an internal clock. */
54
62
  declare class ThreeRig extends Group {
@@ -64,6 +72,8 @@ declare class ThreeRig extends Group {
64
72
  advance(dt: number): void;
65
73
  update(dt: number): void;
66
74
  draw(): PosedFrame;
75
+ renderFrame(frame: PosedFrame | LiveFrame): void;
76
+ private drawCurrent;
67
77
  drawnFrame(): PosedFrame;
68
78
  posedFrame(slots?: SlotFilter | null): PosedFrame;
69
79
  bone(name: string): BoneWorld;
package/dist/index.js CHANGED
@@ -1,3 +1,283 @@
1
+ // packages/shared/src/live.ts
2
+ var EMPTY = Object.freeze([]);
3
+ var immutableLengths = /* @__PURE__ */ new WeakSet();
4
+ var immutableSpans = /* @__PURE__ */ new WeakSet();
5
+ var immutableMetadata = /* @__PURE__ */ new WeakSet();
6
+ var IDENTITY_FIELDS = ["slot", "slotIndex", "attachment", "skin", "placeholder", "kind", "region", "page", "pma", "blend", "offset", "count"];
7
+ function isImmutableSpan(value, indexed) {
8
+ if (!value || typeof value !== "object") return false;
9
+ if (immutableSpans.has(value)) return true;
10
+ if (Object.isFrozen(value) && Array.isArray(value)) {
11
+ const descriptors = Object.getOwnPropertyDescriptors(value);
12
+ for (let i = 0; i < value.length; i++) {
13
+ const d = descriptors[i];
14
+ if (!d || !("value" in d) || typeof d.value !== "number") return false;
15
+ }
16
+ immutableSpans.add(value);
17
+ return true;
18
+ }
19
+ if (!indexed || value.length === 0) return false;
20
+ const d0 = Object.getOwnPropertyDescriptor(value, 0);
21
+ return Boolean(d0 && (!d0.writable || "get" in d0));
22
+ }
23
+ function frozenData(value) {
24
+ if (immutableMetadata.has(value)) return true;
25
+ if (!Object.isFrozen(value)) return false;
26
+ const descriptors = Object.getOwnPropertyDescriptors(value);
27
+ if (!IDENTITY_FIELDS.every((key) => {
28
+ const descriptor = descriptors[key];
29
+ return descriptor && "value" in descriptor && (descriptor.value === null || !["object", "function"].includes(typeof descriptor.value));
30
+ })) return false;
31
+ immutableMetadata.add(value);
32
+ return true;
33
+ }
34
+ function numbers(value, owner, field, slot) {
35
+ if (value && typeof value === "object" && immutableLengths.has(value)) return;
36
+ if (!value || typeof value !== "object" || !Number.isInteger(value.length) || value.length < 0) {
37
+ throw new Error(`${owner}: live ${slot === void 0 ? field : `slot "${slot}" ${field}`} needs an indexed buffer with nonnegative integer length`);
38
+ }
39
+ const length = Object.getOwnPropertyDescriptor(value, "length");
40
+ if (length && "value" in length && !length.writable && !length.configurable && Object.isFrozen(value)) immutableLengths.add(value);
41
+ }
42
+ var NumberSpan = class {
43
+ constructor(count) {
44
+ this.count = count;
45
+ }
46
+ count;
47
+ source = EMPTY;
48
+ offset = 0;
49
+ };
50
+ function numberSource(value) {
51
+ return value instanceof NumberSpan ? value.source : value;
52
+ }
53
+ function numberOffset(value) {
54
+ return value instanceof NumberSpan ? value.offset : 0;
55
+ }
56
+ function numberCount(value) {
57
+ return value instanceof NumberSpan ? value.count : value ? value.length : -1;
58
+ }
59
+ var LiveDrawViews = class {
60
+ constructor(owner = "PixiRenderer") {
61
+ this.owner = owner;
62
+ }
63
+ owner;
64
+ order = [];
65
+ rows = [];
66
+ validated = /* @__PURE__ */ new WeakSet();
67
+ read(frame) {
68
+ void frame.generation;
69
+ const { drawOrder, vertices, tint, attachments, count } = frame;
70
+ const owner = this.owner;
71
+ numbers(drawOrder, owner, "drawOrder");
72
+ numbers(vertices, owner, "vertices");
73
+ numbers(tint, owner, "tint");
74
+ if (!Number.isInteger(count) || count < 0 || count > drawOrder.length) {
75
+ throw new Error(`${owner}: live frame count=${count} needs an integer in 0..${drawOrder.length}`);
76
+ }
77
+ for (let i = 0; i < count; i++) {
78
+ const index = drawOrder[i];
79
+ const attachment = attachments[index];
80
+ if (!Number.isInteger(index) || !attachment || attachment.kind !== "region" && attachment.kind !== "mesh") {
81
+ throw new Error(`${owner}: live drawOrder[${i}]=${index} needs a region or mesh attachment`);
82
+ }
83
+ const a = attachment;
84
+ if (!this.validated.has(a)) {
85
+ if (typeof a.slot !== "string" || a.slot.length === 0 || typeof a.page !== "string" || a.page.length === 0 || a.kind === "region" && (typeof a.region !== "string" || a.region.length === 0)) {
86
+ throw new Error(`${owner}: live slot "${a.slot}" needs named slot, page and region metadata`);
87
+ }
88
+ if (!Number.isInteger(a.offset) || !Number.isInteger(a.count) || a.offset < 0 || a.count < 0 || !Number.isInteger(a.slotIndex) || a.slotIndex < 0) {
89
+ throw new Error(`${owner}: live slot "${a.slot}" has invalid vertex span ${a.offset}+${a.count} or tint slotIndex=${a.slotIndex}`);
90
+ }
91
+ if (frozenData(a)) this.validated.add(a);
92
+ }
93
+ numbers(a.uvs, owner, "uvs", a.slot);
94
+ numbers(a.triangles, owner, "triangles", a.slot);
95
+ if (a.dark !== null) numbers(a.dark, owner, "dark", a.slot);
96
+ if (a.clip !== null) numbers(a.clip.polygon, owner, "clip.polygon", a.slot);
97
+ if (a.offset + a.count > vertices.length || a.slotIndex * 4 + 4 > tint.length) {
98
+ throw new Error(`${owner}: live slot "${a.slot}" has invalid vertex span ${a.offset}+${a.count} or tint slotIndex=${a.slotIndex}`);
99
+ }
100
+ let row = this.rows[i];
101
+ if (!row || row.slot.kind !== a.kind || row.points.count !== a.count) {
102
+ const points = new NumberSpan(a.count);
103
+ const tint2 = new NumberSpan(4);
104
+ const common = { slot: a.slot, blend: a.blend, tint: tint2, page: a.page ?? "", pma: a.pma, uvs: EMPTY };
105
+ const slot2 = a.kind === "region" ? { ...common, kind: "region", region: a.region ?? "", corners: points } : { ...common, kind: "mesh", vertices: points, triangles: EMPTY };
106
+ row = { slot: slot2, points, tint: tint2 };
107
+ this.rows[i] = row;
108
+ }
109
+ row.points.source = vertices;
110
+ row.points.offset = a.offset;
111
+ row.tint.source = tint;
112
+ row.tint.offset = a.slotIndex * 4;
113
+ const slot = row.slot;
114
+ slot.slot = a.slot;
115
+ slot.blend = a.blend;
116
+ slot.page = a.page ?? "";
117
+ slot.pma = a.pma;
118
+ slot.uvs = a.uvs;
119
+ slot.dark = a.dark ?? void 0;
120
+ slot.clip = a.clip ?? void 0;
121
+ if (slot.kind === "region") slot.region = a.region ?? "";
122
+ else slot.triangles = a.triangles;
123
+ this.order[i] = slot;
124
+ }
125
+ this.order.length = count;
126
+ return this.order;
127
+ }
128
+ release() {
129
+ for (const row of this.rows) {
130
+ row.points.source = EMPTY;
131
+ row.tint.source = EMPTY;
132
+ row.slot.uvs = EMPTY;
133
+ row.slot.dark = void 0;
134
+ row.slot.clip = void 0;
135
+ if (row.slot.kind === "mesh") row.slot.triangles = EMPTY;
136
+ }
137
+ }
138
+ };
139
+ function identity(a) {
140
+ return JSON.stringify([a.slotIndex, a.slot, a.skin, a.placeholder, a.attachment, a.kind, a.page, a.region]);
141
+ }
142
+ function templateFromAttachment(a) {
143
+ const common = {
144
+ slot: a.slot,
145
+ z: a.slotIndex,
146
+ blend: a.blend,
147
+ tint: [1, 1, 1, 1]
148
+ };
149
+ return a.kind === "region" ? {
150
+ ...common,
151
+ kind: "region",
152
+ region: a.region ?? "",
153
+ corners: [],
154
+ page: a.page ?? "",
155
+ pma: a.pma,
156
+ uvs: Array.from(a.uvs)
157
+ } : {
158
+ ...common,
159
+ kind: "mesh",
160
+ page: a.page ?? "",
161
+ pma: a.pma,
162
+ vertices: [],
163
+ uvs: Array.from(a.uvs),
164
+ triangles: Array.from(a.triangles),
165
+ sequence: 0
166
+ };
167
+ }
168
+ function copyNumbers(target, source) {
169
+ if (ArrayBuffer.isView(source) && source instanceof Float64Array) {
170
+ target.set(source);
171
+ } else {
172
+ for (let i = 0; i < target.length; i++) target[i] = source[i];
173
+ }
174
+ }
175
+ function savedNumbers(source, offset, count) {
176
+ const result = new Array(count);
177
+ for (let i = 0; i < count; i++) result[i] = source[offset + i];
178
+ return result;
179
+ }
180
+ var RetainedRuntimeFrame = class {
181
+ constructor(owner = "PixiRig") {
182
+ this.owner = owner;
183
+ }
184
+ owner;
185
+ templates = /* @__PURE__ */ new WeakMap();
186
+ immutable = /* @__PURE__ */ new WeakSet();
187
+ checkedTemplates = /* @__PURE__ */ new Map();
188
+ rows = [];
189
+ vertices = new Float64Array(0);
190
+ tint = new Float64Array(0);
191
+ count = 0;
192
+ generation = 0;
193
+ retained = null;
194
+ capture(frame, seed) {
195
+ let snapshot = null;
196
+ const { count, attachments, drawOrder, vertices, tint } = frame;
197
+ if (this.vertices.length !== vertices.length) this.vertices = new Float64Array(vertices.length);
198
+ if (this.tint.length !== tint.length) this.tint = new Float64Array(tint.length);
199
+ copyNumbers(this.vertices, vertices);
200
+ copyNumbers(this.tint, tint);
201
+ for (let i = 0; i < count; i++) {
202
+ const a = attachments[drawOrder[i]];
203
+ let template = this.immutable.has(a) ? this.templates.get(a) : void 0;
204
+ if (!template) {
205
+ const key = identity(a);
206
+ template = this.checkedTemplates.get(key);
207
+ if (!template) {
208
+ if (seed) {
209
+ snapshot ??= seed();
210
+ template = snapshot.order[i];
211
+ if (!template || template.slot !== a.slot || template.kind !== a.kind || template.page !== a.page) {
212
+ throw new Error(`${this.owner}: live slot "${a.slot}" needs a matching snapshot metadata seed at draw index ${i}`);
213
+ }
214
+ } else {
215
+ template = templateFromAttachment(a);
216
+ }
217
+ if (this.checkedTemplates.size >= Math.max(64, frame.attachments.length * 2)) this.checkedTemplates.clear();
218
+ this.checkedTemplates.set(key, template);
219
+ }
220
+ if (frozenData(a)) {
221
+ this.immutable.add(a);
222
+ this.templates.set(a, template);
223
+ }
224
+ }
225
+ let row = this.rows[i];
226
+ if (!row) {
227
+ row = { template, offset: a.offset, count: a.count, tintOffset: a.slotIndex * 4 };
228
+ this.rows[i] = row;
229
+ }
230
+ row.template = template;
231
+ row.offset = a.offset;
232
+ row.count = a.count;
233
+ row.tintOffset = a.slotIndex * 4;
234
+ if (a.dark === null) row.dark = void 0;
235
+ else {
236
+ const dark = row.darkBuffer ??= [];
237
+ dark.length = a.dark.length;
238
+ for (let j = 0; j < dark.length; j++) dark[j] = a.dark[j];
239
+ row.dark = dark;
240
+ }
241
+ if (a.clip === null) row.clip = void 0;
242
+ else {
243
+ const clip = row.clipBuffer ??= { polygon: [], inverse: false, convex: true };
244
+ clip.polygon.length = a.clip.polygon.length;
245
+ for (let j = 0; j < clip.polygon.length; j++) clip.polygon[j] = a.clip.polygon[j];
246
+ clip.inverse = a.clip.inverse;
247
+ clip.convex = a.clip.convex;
248
+ row.clip = clip;
249
+ }
250
+ }
251
+ this.count = frame.count;
252
+ this.generation = frame.generation;
253
+ this.retained = null;
254
+ }
255
+ snapshot() {
256
+ if (this.retained) return this.retained;
257
+ const order = [];
258
+ for (let i = 0; i < this.count; i++) {
259
+ const { template, offset, count, tintOffset, dark, clip } = this.rows[i];
260
+ const common = { ...template, tint: savedNumbers(this.tint, tintOffset, 4) };
261
+ if (dark) common.dark = Array.from(dark);
262
+ else delete common.dark;
263
+ if (clip) common.clip = {
264
+ polygon: Array.from(clip.polygon),
265
+ inverse: clip.inverse,
266
+ convex: clip.convex
267
+ };
268
+ else delete common.clip;
269
+ order.push(template.kind === "region" ? { ...common, kind: "region", region: template.region, corners: savedNumbers(this.vertices, offset, count) } : { ...common, kind: "mesh", vertices: savedNumbers(this.vertices, offset, count), triangles: template.triangles, sequence: template.sequence });
270
+ }
271
+ this.retained = { order };
272
+ Object.defineProperty(this.retained, "generation", { value: this.generation, enumerable: false, configurable: true });
273
+ return this.retained;
274
+ }
275
+ /** Explicit draw() already requested a detached snapshot. */
276
+ retain(frame) {
277
+ this.retained = frame;
278
+ }
279
+ };
280
+
1
281
  // packages/three/src/three.ts
2
282
  import { RigPlayer } from "@rig-c/play";
3
283
  import {
@@ -39,11 +319,12 @@ function hull(points) {
39
319
  };
40
320
  return [...half(sorted).slice(0, -1), ...half([...sorted].reverse()).slice(0, -1)];
41
321
  }
42
- function clipPolygon(clip, detail, capacity) {
322
+ function clipPolygon(clip, detail, capacity, indexed = false) {
43
323
  const fail = (why) => {
44
324
  throw new Error(`${detail} has invalid clip: ${why} (expected finite simple nonzero-area polygon, boolean inverse/convex, at most GPU MAX_TEXTURE_SIZE=${capacity} vertices)`);
45
325
  };
46
- if (!clip || typeof clip.inverse !== "boolean" || typeof clip.convex !== "boolean" || !Array.isArray(clip.polygon) || clip.polygon.length < 6 || clip.polygon.length % 2 !== 0) fail("invalid flags or polygon length");
326
+ const validPolygon = indexed ? Boolean(clip && clip.polygon && typeof clip.polygon === "object" && Number.isInteger(clip.polygon.length)) : Boolean(clip && Array.isArray(clip.polygon));
327
+ if (!clip || typeof clip.inverse !== "boolean" || typeof clip.convex !== "boolean" || !validPolygon || clip.polygon.length < 6 || clip.polygon.length % 2 !== 0) fail("invalid flags or polygon length");
47
328
  if (clip.polygon.length / 2 > capacity) fail(`THREE_CLIP_CAPACITY: found ${clip.polygon.length / 2} vertices`);
48
329
  const points = [];
49
330
  for (let i = 0; i < clip.polygon.length; i += 2) {
@@ -80,7 +361,7 @@ var livePages = /* @__PURE__ */ new Set();
80
361
  var getLivePageTextureCount = () => livePages.size;
81
362
  var getPageRefCount = (image) => pages.get(image)?.holders.size ?? 0;
82
363
  var getSharedPageTexture = (image) => pages.get(image)?.texture;
83
- var REGION_INDICES = [0, 1, 2, 2, 3, 0];
364
+ var REGION_INDICES = Object.freeze([0, 1, 2, 2, 3, 0]);
84
365
  var VERTEX = `
85
366
  out vec2 vUV;
86
367
  out vec2 vPosition;
@@ -137,6 +418,16 @@ var ThreeRenderer = class {
137
418
  pool = /* @__PURE__ */ new Map();
138
419
  materials = /* @__PURE__ */ new Map();
139
420
  retained = /* @__PURE__ */ new Set();
421
+ liveViews = new LiveDrawViews("ThreeRenderer");
422
+ retainedRuntimeFrame = new RetainedRuntimeFrame("ThreeRenderer");
423
+ accepted = null;
424
+ hasRetained = false;
425
+ get drawnFrame() {
426
+ if (this.accepted) return this.accepted;
427
+ if (!this.hasRetained) return null;
428
+ this.accepted = this.retainedRuntimeFrame.snapshot();
429
+ return this.accepted;
430
+ }
140
431
  get geometryUploadCount() {
141
432
  return this.positionUploadCount + this.uvUploadCount + this.triangleUploadCount;
142
433
  }
@@ -150,7 +441,7 @@ var ThreeRenderer = class {
150
441
  const image = this.assets.pageImages.get(name);
151
442
  return image && this.retained.has(image) ? pages.get(image)?.texture : void 0;
152
443
  }
153
- prepare(slot) {
444
+ prepare(slot, key, indexed) {
154
445
  const detail = `ThreeRenderer: slot "${slot?.slot ?? "<unnamed>"}" on page "${slot?.page ?? "<unnamed>"}"`;
155
446
  const fail = (why) => {
156
447
  throw new Error(`${detail}: ${why}`);
@@ -159,26 +450,58 @@ var ThreeRenderer = class {
159
450
  if (typeof slot.slot !== "string" || slot.slot.length === 0 || typeof slot.page !== "string" || slot.page.length === 0) fail("expected non-empty slot and page names");
160
451
  if (!["Normal", "Additive", "Multiply", "Screen"].includes(slot.blend)) fail(`blend=${slot.blend} (expected Normal, Additive, Multiply, Screen)`);
161
452
  if (typeof slot.pma !== "boolean") fail(`pma=${String(slot.pma)} (expected boolean)`);
162
- if (!Number.isFinite(slot.z)) fail(`z=${String(slot.z)} (expected finite draw-order index)`);
453
+ if (!indexed && !Number.isFinite(slot.z)) fail(`z=${String(slot.z)} (expected finite draw-order index)`);
163
454
  const image = this.assets.pageImages.get(slot.page);
164
455
  if (!image || !(image.naturalWidth > 0 && image.naturalHeight > 0)) fail("page image absent or not decoded (expected positive naturalWidth/naturalHeight)");
165
456
  const valid = (values, count, field) => {
166
- if (!Array.isArray(values) || values.length !== count) fail(`${field} length=${values?.length} (expected ${count})`);
167
- for (let i = 0; i < count; i++) if (!Number.isFinite(values[i]) || !Number.isFinite(Math.fround(values[i]))) fail(`${field}[${i}]=${String(values[i])} (expected finite float32)`);
457
+ const source = numberSource(values), offset = numberOffset(values), length = numberCount(values);
458
+ if (!indexed && !Array.isArray(values) || length !== count) fail(`${field} length=${Array.isArray(values) || indexed ? length : void 0} (expected ${count})`);
459
+ for (let i = 0; i < count; i++) {
460
+ const value = source[offset + i];
461
+ if (!Number.isFinite(value) || !Number.isFinite(Math.fround(value))) fail(`${field}[${i}]=${String(value)} (expected finite float32)`);
462
+ }
168
463
  };
169
464
  valid(slot.tint, 4, "tint");
170
465
  if (slot.dark !== void 0) valid(slot.dark, 3, "dark");
171
466
  const coords = slot.kind === "region" ? slot.corners : slot.vertices;
172
- if (!Array.isArray(coords) || coords.length < 6 || coords.length % 2 !== 0) fail(`positions length=${coords?.length} (expected even length >=6)`);
173
- valid(coords, slot.kind === "region" ? 8 : coords.length, slot.kind === "region" ? "corners" : "vertices");
174
- valid(slot.uvs, coords.length, "uvs");
467
+ const coordCount = numberCount(coords);
468
+ if (!indexed && !Array.isArray(coords) || coordCount < 6 || coordCount % 2 !== 0) fail(`positions length=${Array.isArray(coords) || indexed ? coordCount : void 0} (expected even length >=6)`);
469
+ valid(coords, slot.kind === "region" ? 8 : coordCount, slot.kind === "region" ? "corners" : "vertices");
470
+ const prior = this.pool.get(key);
471
+ const priorPositions = prior?.mesh.geometry.getAttribute("position");
472
+ const sameVertexCount = !this.alwaysReuploadGeometry && prior !== void 0 && priorPositions !== void 0 && priorPositions.array.length === coordCount / 2 * 3;
473
+ if (!sameVertexCount || !isImmutableSpan(slot.uvs, indexed) || prior.uvs !== slot.uvs) {
474
+ valid(slot.uvs, coordCount, "uvs");
475
+ }
175
476
  const indices = slot.kind === "region" ? REGION_INDICES : slot.triangles;
176
- if (!Array.isArray(indices) || indices.length < 3 || indices.length % 3 !== 0) fail(`triangles length=${indices?.length} (expected multiple of three >=3)`);
177
- indices.forEach((v, i) => {
178
- if (!Number.isInteger(v) || v < 0 || v >= coords.length / 2) fail(`triangles[${i}]=${v} (expected integer 0..${coords.length / 2 - 1})`);
179
- });
180
- if (slot.kind === "mesh" && !Number.isInteger(slot.sequence)) fail(`sequence=${slot.sequence} (expected integer)`);
181
- return { slot, image, positions: coords, indices, polygon: slot.clip !== void 0 ? clipPolygon(slot.clip, detail, this.host.capabilities.maxTextureSize) : null };
477
+ if (!sameVertexCount || !isImmutableSpan(indices, indexed) || prior.indices !== indices) {
478
+ if (!indexed && !Array.isArray(indices) || indices.length < 3 || indices.length % 3 !== 0) fail(`triangles length=${indices?.length} (expected multiple of three >=3)`);
479
+ for (let i = 0; i < indices.length; i++) {
480
+ const v = indices[i];
481
+ if (!Number.isInteger(v) || v < 0 || v >= coordCount / 2) fail(`triangles[${i}]=${v} (expected integer 0..${coordCount / 2 - 1})`);
482
+ }
483
+ }
484
+ if (!indexed && slot.kind === "mesh" && !Number.isInteger(slot.sequence)) fail(`sequence=${slot.sequence} (expected integer)`);
485
+ const tintSource = numberSource(slot.tint), tintOffset = numberOffset(slot.tint);
486
+ const light = [
487
+ tintSource[tintOffset + 0],
488
+ tintSource[tintOffset + 1],
489
+ tintSource[tintOffset + 2],
490
+ tintSource[tintOffset + 3]
491
+ ];
492
+ const dark = slot.dark ? [slot.dark[0], slot.dark[1], slot.dark[2]] : [0, 0, 0];
493
+ return {
494
+ slot,
495
+ key,
496
+ image,
497
+ positions: coords,
498
+ indices,
499
+ polygon: slot.clip !== void 0 ? clipPolygon(slot.clip, detail, this.host.capabilities.maxTextureSize, indexed) : null,
500
+ light,
501
+ dark,
502
+ inverse: slot.clip?.inverse ?? false,
503
+ blend: slot.blend
504
+ };
182
505
  }
183
506
  material(item) {
184
507
  let material = this.materials.get(item.image);
@@ -216,71 +539,162 @@ var ThreeRenderer = class {
216
539
  }
217
540
  render(frame) {
218
541
  if (this.disposed) throw new Error("ThreeRenderer: render called after dispose (expected active renderer)");
219
- if (!frame || !Array.isArray(frame.order)) throw new Error("ThreeRenderer: frame.order absent (expected PosedSlot array)");
220
- const prepared = frame.order.map((s) => this.prepare(s));
542
+ if (!frame || typeof frame !== "object") throw new Error("ThreeRenderer: frame.order absent (expected PosedSlot array)");
543
+ if ("order" in frame) {
544
+ if (!Array.isArray(frame.order)) throw new Error("ThreeRenderer: frame.order absent (expected PosedSlot array)");
545
+ this.renderOrder(frame.order, false);
546
+ this.accepted = frame;
547
+ this.hasRetained = false;
548
+ return;
549
+ }
550
+ try {
551
+ this.renderOrder(this.liveViews.read(frame), true);
552
+ this.retainedRuntimeFrame.capture(frame);
553
+ this.accepted = null;
554
+ this.hasRetained = true;
555
+ } finally {
556
+ this.liveViews.release();
557
+ }
558
+ }
559
+ renderOrder(order, indexed) {
560
+ const occurrences = /* @__PURE__ */ new Map();
561
+ const prepared = order.map((s) => {
562
+ const slotName = typeof s?.slot === "string" && s.slot.length > 0 ? s.slot : "<unnamed>";
563
+ const occurrence = occurrences.get(slotName) ?? 0;
564
+ occurrences.set(slotName, occurrence + 1);
565
+ return this.prepare(s, `${slotName}\0${occurrence}`, indexed);
566
+ });
221
567
  const conventions = /* @__PURE__ */ new Map();
222
568
  for (const item of prepared) {
223
569
  const pma = conventions.get(item.image) ?? pages.get(item.image)?.pma;
224
570
  if (pma !== void 0 && pma !== item.slot.pma) throw new Error(`ThreeRenderer: slot "${item.slot.slot}" page "${item.slot.page}" pma=${item.slot.pma} conflicts with retained pma=${pma} (expected one convention per page image)`);
225
571
  conventions.set(item.image, item.slot.pma);
226
572
  }
227
- const occurrences = /* @__PURE__ */ new Map();
228
573
  const active = [];
229
574
  prepared.forEach((item, index) => {
230
- const occurrence = occurrences.get(item.slot.slot) ?? 0;
231
- occurrences.set(item.slot.slot, occurrence + 1);
232
- const key = `${item.slot.slot}\0${occurrence}`;
575
+ const { key } = item;
233
576
  let entry = this.pool.get(key);
234
577
  if (!entry) {
235
578
  const geometry = new BufferGeometry();
236
579
  const mesh2 = new Mesh(geometry, this.material(item));
237
580
  mesh2.frustumCulled = false;
238
581
  mesh2.name = item.slot.slot;
239
- entry = { mesh: mesh2, polygon: null, polygonValues: [], uvs: null, indices: null };
582
+ entry = {
583
+ mesh: mesh2,
584
+ polygon: null,
585
+ polygonValues: [],
586
+ uvs: null,
587
+ indices: null,
588
+ light: item.light,
589
+ dark: item.dark,
590
+ inverse: item.inverse,
591
+ blend: item.blend,
592
+ needsWideIndex: false
593
+ };
594
+ const current = entry;
595
+ mesh2.onBeforeRender = () => {
596
+ const material = mesh2.material;
597
+ material.uniforms.light.value = current.light;
598
+ material.uniforms.dark.value = current.dark;
599
+ material.uniforms.polygon.value = current.polygon ?? material.uniforms.page.value;
600
+ material.uniforms.points.value = current.polygon === null ? 0 : current.polygonValues.length / 2;
601
+ material.uniforms.inverse.value = current.inverse;
602
+ material.blendEquation = material.blendEquationAlpha = AddEquation;
603
+ material.blendSrc = current.blend === "Multiply" ? DstColorFactor : OneFactor;
604
+ material.blendDst = current.blend === "Additive" ? OneFactor : current.blend === "Screen" ? OneMinusSrcColorFactor : OneMinusSrcAlphaFactor;
605
+ material.blendSrcAlpha = OneFactor;
606
+ material.blendDstAlpha = material.blendDst;
607
+ material.uniformsNeedUpdate = true;
608
+ };
240
609
  this.pool.set(key, entry);
241
610
  }
242
611
  const { mesh } = entry;
612
+ const positionCount = numberCount(item.positions);
243
613
  const oldPositions = mesh.geometry.getAttribute("position"), oldUVs = mesh.geometry.getAttribute("uv"), oldIndex = mesh.geometry.getIndex();
244
- const needsWideIndex = item.indices.some((v) => v >= 65535);
245
- if (oldPositions && oldPositions.array.length !== item.positions.length / 2 * 3 || oldUVs && oldUVs.array.length !== item.slot.uvs.length || oldIndex && (oldIndex.count !== item.indices.length || needsWideIndex && !(oldIndex.array instanceof Uint32Array))) {
614
+ let needsWideIndex = entry.needsWideIndex;
615
+ if (this.alwaysReuploadGeometry || entry.indices !== item.indices || !oldIndex) {
616
+ needsWideIndex = false;
617
+ if (positionCount / 2 > 65535) {
618
+ for (let i = 0; i < item.indices.length; i++) if (item.indices[i] >= 65535) {
619
+ needsWideIndex = true;
620
+ break;
621
+ }
622
+ }
623
+ entry.needsWideIndex = needsWideIndex;
624
+ }
625
+ if (oldPositions && oldPositions.array.length !== positionCount / 2 * 3 || oldUVs && oldUVs.array.length !== item.slot.uvs.length || oldIndex && (oldIndex.count !== item.indices.length || needsWideIndex && !(oldIndex.array instanceof Uint32Array))) {
246
626
  mesh.geometry.dispose();
247
627
  mesh.geometry = new BufferGeometry();
248
628
  entry.uvs = entry.indices = null;
249
629
  }
250
- mesh.posedSlot = item.slot;
630
+ entry.light = item.light;
631
+ entry.dark = item.dark;
632
+ entry.inverse = item.inverse;
633
+ entry.blend = item.blend;
634
+ if (!indexed) {
635
+ mesh.posedSlot = item.slot;
636
+ } else {
637
+ const prior = mesh.posedSlot;
638
+ if (!prior || prior.slot !== item.slot.slot || prior.page !== item.slot.page || prior.pma !== item.slot.pma || prior.kind !== item.slot.kind || prior.blend !== item.slot.blend) {
639
+ mesh.posedSlot = item.slot.kind === "region" ? { kind: "region", slot: item.slot.slot, z: 0, page: item.slot.page, region: item.slot.region, pma: item.slot.pma, blend: item.slot.blend, corners: [], uvs: [], tint: item.light } : { kind: "mesh", slot: item.slot.slot, z: 0, page: item.slot.page, pma: item.slot.pma, blend: item.slot.blend, vertices: [], uvs: [], triangles: [], sequence: 0, tint: item.light };
640
+ }
641
+ }
251
642
  mesh.material = this.material(item);
252
643
  mesh.renderOrder = index;
253
- const update = (name, values, size, force) => {
254
- let attribute = mesh.geometry.getAttribute(name);
255
- let changed = force || !attribute || attribute.array.length !== values.length;
256
- if (!attribute || attribute.array.length !== values.length) {
257
- attribute = new BufferAttribute(new Float32Array(values.length), size).setUsage(DynamicDrawUsage);
258
- mesh.geometry.setAttribute(name, attribute);
644
+ const posLength = positionCount / 2 * 3;
645
+ let posAttr = mesh.geometry.getAttribute("position");
646
+ let posChanged = this.alwaysReuploadGeometry || !posAttr || posAttr.array.length !== posLength;
647
+ if (!posAttr || posAttr.array.length !== posLength) {
648
+ posAttr = new BufferAttribute(new Float32Array(posLength), 3).setUsage(DynamicDrawUsage);
649
+ mesh.geometry.setAttribute("position", posAttr);
650
+ }
651
+ const posArray = posAttr.array;
652
+ const posSource = numberSource(item.positions), posOffset = numberOffset(item.positions);
653
+ for (let i = 0, out = 0; i < positionCount; i += 2, out += 3) {
654
+ const x = Math.fround(posSource[posOffset + i]), y = Math.fround(posSource[posOffset + i + 1]);
655
+ if (posArray[out] !== x || posArray[out + 1] !== y || posArray[out + 2] !== 0) {
656
+ posArray[out] = x;
657
+ posArray[out + 1] = y;
658
+ posArray[out + 2] = 0;
659
+ posChanged = true;
660
+ }
661
+ }
662
+ if (posChanged) {
663
+ posAttr.needsUpdate = true;
664
+ this.positionUploadCount++;
665
+ }
666
+ if (this.alwaysReuploadGeometry || entry.uvs !== item.slot.uvs) {
667
+ const uvs = item.slot.uvs;
668
+ let uvAttr = mesh.geometry.getAttribute("uv");
669
+ let uvChanged = true;
670
+ if (!uvAttr || uvAttr.array.length !== uvs.length) {
671
+ uvAttr = new BufferAttribute(new Float32Array(uvs.length), 2).setUsage(DynamicDrawUsage);
672
+ mesh.geometry.setAttribute("uv", uvAttr);
259
673
  }
260
- const array = attribute.array;
261
- for (let i = 0; i < values.length; i++) if (array[i] !== Math.fround(values[i])) {
262
- array[i] = values[i];
263
- changed = true;
674
+ const uvArray = uvAttr.array;
675
+ for (let i = 0; i < uvs.length; i++) {
676
+ const v = Math.fround(uvs[i]);
677
+ if (uvArray[i] !== v) {
678
+ uvArray[i] = v;
679
+ uvChanged = true;
680
+ }
264
681
  }
265
- if (changed) {
266
- attribute.needsUpdate = true;
267
- if (name === "position") this.positionUploadCount++;
268
- else this.uvUploadCount++;
682
+ if (uvChanged) {
683
+ uvAttr.needsUpdate = true;
684
+ this.uvUploadCount++;
269
685
  }
270
- };
271
- const xyz = [];
272
- for (let i = 0; i < item.positions.length; i += 2) xyz.push(item.positions[i], item.positions[i + 1], 0);
273
- update("position", xyz, 3, this.alwaysReuploadGeometry);
274
- if (this.alwaysReuploadGeometry || entry.uvs !== item.slot.uvs) {
275
- update("uv", item.slot.uvs, 2, true);
276
- entry.uvs = item.slot.uvs;
686
+ entry.uvs = uvs;
277
687
  }
278
688
  if (this.alwaysReuploadGeometry || entry.indices !== item.indices) {
279
689
  const before = mesh.geometry.getIndex();
280
690
  if (before && before.count === item.indices.length) {
281
691
  for (let i = 0; i < item.indices.length; i++) before.setX(i, item.indices[i]);
282
692
  before.needsUpdate = true;
283
- } else mesh.geometry.setIndex([...item.indices]);
693
+ } else {
694
+ const copied = new Array(item.indices.length);
695
+ for (let i = 0; i < item.indices.length; i++) copied[i] = item.indices[i];
696
+ mesh.geometry.setIndex(copied);
697
+ }
284
698
  this.triangleUploadCount++;
285
699
  entry.indices = item.indices;
286
700
  }
@@ -300,21 +714,6 @@ var ThreeRenderer = class {
300
714
  entry.polygon = null;
301
715
  entry.polygonValues = [];
302
716
  }
303
- const current = entry;
304
- mesh.onBeforeRender = () => {
305
- const material = mesh.material, slot = mesh.posedSlot;
306
- material.uniforms.light.value = slot.tint;
307
- material.uniforms.dark.value = slot.dark ?? [0, 0, 0];
308
- material.uniforms.polygon.value = current.polygon ?? material.uniforms.page.value;
309
- material.uniforms.points.value = current.polygon === null ? 0 : current.polygonValues.length / 2;
310
- material.uniforms.inverse.value = slot.clip?.inverse ?? false;
311
- material.blendEquation = material.blendEquationAlpha = AddEquation;
312
- material.blendSrc = slot.blend === "Multiply" ? DstColorFactor : OneFactor;
313
- material.blendDst = slot.blend === "Additive" ? OneFactor : slot.blend === "Screen" ? OneMinusSrcColorFactor : OneMinusSrcAlphaFactor;
314
- material.blendSrcAlpha = OneFactor;
315
- material.blendDstAlpha = material.blendDst;
316
- material.uniformsNeedUpdate = true;
317
- };
318
717
  active.push(mesh);
319
718
  });
320
719
  for (const entry of this.pool.values()) if (entry.mesh.parent === this.root) this.root.remove(entry.mesh);
@@ -361,7 +760,13 @@ var ThreeRig = class extends Group {
361
760
  if (!assets?.document || !(assets.pageImages instanceof Map) || !isWebGLHost(options?.renderer)) throw new Error("ThreeRig: expected ModelAssets and options.renderer WebGLRenderer");
362
761
  const scale = options.timescale ?? options.timeScale ?? 1;
363
762
  if (!Number.isFinite(scale) || scale < 0) throw new Error(`ThreeRig: timescale=${scale} (expected finite number >=0)`);
364
- this.player = new RigPlayer(document.createElement("div"), assets, { mode: assets.regions === "none" ? "canvas" : "dom", skin: options.skin, slots: options.slots });
763
+ this.player = new RigPlayer(document.createElement("div"), assets, {
764
+ mode: assets.regions === "none" ? "canvas" : "dom",
765
+ skin: options.skin,
766
+ slots: options.slots,
767
+ liveFrameChecks: options.liveFrameChecks,
768
+ poser: options.poser
769
+ });
365
770
  this.renderer = new ThreeRenderer(this, assets, options.renderer);
366
771
  this.timescale = scale;
367
772
  try {
@@ -375,7 +780,7 @@ var ThreeRig = class extends Group {
375
780
  assets;
376
781
  player;
377
782
  renderer;
378
- lastFrame;
783
+ lastFrame = null;
379
784
  destroyed = false;
380
785
  active() {
381
786
  if (this.destroyed) throw new Error("ThreeRig: called after destroy (expected active rig)");
@@ -383,20 +788,20 @@ var ThreeRig = class extends Group {
383
788
  play(name, options = {}) {
384
789
  this.active();
385
790
  this.player.play(name, options);
386
- this.draw();
791
+ this.drawCurrent();
387
792
  }
388
793
  seek(time) {
389
794
  this.active();
390
795
  if (!Number.isFinite(time)) throw new Error(`ThreeRig: time=${time} (expected finite seconds)`);
391
796
  this.player.seek(time);
392
- this.draw();
797
+ this.drawCurrent();
393
798
  }
394
799
  advance(dt) {
395
800
  this.active();
396
801
  if (!Number.isFinite(dt) || dt < 0) throw new Error(`ThreeRig: dt=${dt} (expected finite seconds >=0)`);
397
802
  if (this.timescale === 0) return;
398
803
  this.player.advance(dt);
399
- this.draw();
804
+ this.drawCurrent();
400
805
  }
401
806
  update(dt) {
402
807
  this.advance(dt);
@@ -408,9 +813,29 @@ var ThreeRig = class extends Group {
408
813
  this.lastFrame = frame;
409
814
  return frame;
410
815
  }
816
+ renderFrame(frame) {
817
+ this.active();
818
+ if ("order" in frame) {
819
+ this.renderer.render(frame);
820
+ this.lastFrame = frame;
821
+ return;
822
+ }
823
+ if (frame.generation !== this.player.generation) {
824
+ throw new Error(`ThreeRig: live frame generation ${frame.generation} needs current player generation ${this.player.generation}`);
825
+ }
826
+ if (this.player.poser !== "runtime") {
827
+ this.renderFrame(this.player.posedFrame());
828
+ return;
829
+ }
830
+ this.renderer.render(frame);
831
+ this.lastFrame = null;
832
+ }
833
+ drawCurrent() {
834
+ this.renderFrame(this.player.poser === "runtime" ? this.player.liveFrame() : this.player.posedFrame());
835
+ }
411
836
  drawnFrame() {
412
837
  this.active();
413
- return this.lastFrame;
838
+ return this.lastFrame ??= this.renderer.drawnFrame;
414
839
  }
415
840
  posedFrame(slots) {
416
841
  this.active();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rig-c/three",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Play rig-c model documents in a native Three.js scene through @rig-c/play.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,7 +28,7 @@
28
28
  "sideEffects": false,
29
29
  "peerDependencies": {
30
30
  "three": "^0.186.1",
31
- "@rig-c/play": "^1.10.0"
31
+ "@rig-c/play": "^1.20.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@rig-c/adapters-shared": "workspace:*",