@rig-c/three 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/rig-c/adapters/main/packages/three/assets/banner.svg" alt="@rig-c/three — Draw a rig-c model in a native Three.js scene through @rig-c/play" width="100%" />
3
+ </p>
4
+
1
5
  # @rig-c/three
2
6
 
3
7
  Draw a rig-c model in a native Three.js scene through `@rig-c/play`. `ThreeRig`
@@ -40,15 +44,17 @@ requestAnimationFrame(frame);
40
44
  ```
41
45
 
42
46
  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.
47
+ 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
48
 
45
49
  ## Live object
46
50
 
47
51
  `play(name, options)`, `seek(seconds)`, `advance(dt)` and `update(dt)` forward to
48
- an independent `RigPlayer`; deltas are seconds, and the player's `timescale`
52
+ an independent `RigPlayer` and consume its `liveFrame()` lease synchronously
53
+ (`liveFrameChecks`, `poser`, `renderFrame(frame)`); deltas are seconds, and the player's `timescale`
49
54
  (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()`
55
+ holds the drawn frame and uploads no geometry. `draw()` returns a detached
56
+ `PosedFrame`; `drawnFrame()` returns a detached snapshot of the last draw,
57
+ including after direct mutations of `rig.player`; `posedFrame(filter)`, `bone(name)` and `bounds()`
52
58
  use play's readers. World bounds are in rig-local Y-up space, before this Group's
53
59
  transform. `resourceCounts()` reports owned meshes, geometries, page materials,
54
60
  clip textures and retained/shared pages. `destroy()` is idempotent and frees
@@ -64,7 +70,7 @@ both straight and premultiplied pixels: conflicting `pma` records are refused.
64
70
  ## Native drawing and scene placement
65
71
 
66
72
  `ThreeRenderer(root, assets, webglRenderer)` is available when the app owns the
67
- player. Call `render(player.posedFrame())`, then render the scene normally.
73
+ player. Call `render(player.liveFrame())` (or `render(player.posedFrame())`), then render the scene normally.
68
74
  No internal canvas, offscreen bitmap of the rig, or fallback renderer draws it.
69
75
  Positions and page UVs come directly from the record; the full atlas page is
70
76
  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,293 @@
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
+ // Only a numeric token survives release: no borrowed attachment or buffer
67
+ // reference is retained to identify an immutable metadata row.
68
+ validated = /* @__PURE__ */ new WeakMap();
69
+ nextIdentity = 1;
70
+ read(frame) {
71
+ void frame.generation;
72
+ const { drawOrder, vertices, tint, attachments, count } = frame;
73
+ const owner = this.owner;
74
+ numbers(drawOrder, owner, "drawOrder");
75
+ numbers(vertices, owner, "vertices");
76
+ numbers(tint, owner, "tint");
77
+ if (!Number.isInteger(count) || count < 0 || count > drawOrder.length) {
78
+ throw new Error(`${owner}: live frame count=${count} needs an integer in 0..${drawOrder.length}`);
79
+ }
80
+ for (let i = 0; i < count; i++) {
81
+ const index = drawOrder[i];
82
+ const attachment = attachments[index];
83
+ if (!Number.isInteger(index) || !attachment || attachment.kind !== "region" && attachment.kind !== "mesh") {
84
+ throw new Error(`${owner}: live drawOrder[${i}]=${index} needs a region or mesh attachment`);
85
+ }
86
+ const a = attachment;
87
+ let identity2 = this.validated.get(a) ?? 0;
88
+ if (identity2 === 0) {
89
+ 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)) {
90
+ throw new Error(`${owner}: live slot "${a.slot}" needs named slot, page and region metadata`);
91
+ }
92
+ if (!Number.isInteger(a.offset) || !Number.isInteger(a.count) || a.offset < 0 || a.count < 0 || !Number.isInteger(a.slotIndex) || a.slotIndex < 0) {
93
+ throw new Error(`${owner}: live slot "${a.slot}" has invalid vertex span ${a.offset}+${a.count} or tint slotIndex=${a.slotIndex}`);
94
+ }
95
+ if (frozenData(a)) {
96
+ identity2 = this.nextIdentity++;
97
+ this.validated.set(a, identity2);
98
+ }
99
+ }
100
+ numbers(a.uvs, owner, "uvs", a.slot);
101
+ numbers(a.triangles, owner, "triangles", a.slot);
102
+ if (a.dark !== null) numbers(a.dark, owner, "dark", a.slot);
103
+ if (a.clip !== null) numbers(a.clip.polygon, owner, "clip.polygon", a.slot);
104
+ if (a.offset + a.count > vertices.length || a.slotIndex * 4 + 4 > tint.length) {
105
+ throw new Error(`${owner}: live slot "${a.slot}" has invalid vertex span ${a.offset}+${a.count} or tint slotIndex=${a.slotIndex}`);
106
+ }
107
+ let row = this.rows[i];
108
+ if (!row || row.slot.kind !== a.kind || row.points.count !== a.count) {
109
+ const points = new NumberSpan(a.count);
110
+ const tint2 = new NumberSpan(4);
111
+ const common = { slot: a.slot, blend: a.blend, tint: tint2, page: a.page ?? "", pma: a.pma, uvs: EMPTY };
112
+ const slot2 = a.kind === "region" ? { ...common, kind: "region", region: a.region ?? "", corners: points } : { ...common, kind: "mesh", vertices: points, triangles: EMPTY };
113
+ row = { slot: slot2, points, tint: tint2, identity: 0 };
114
+ this.rows[i] = row;
115
+ }
116
+ row.points.source = vertices;
117
+ row.tint.source = tint;
118
+ const slot = row.slot;
119
+ if (identity2 === 0 || row.identity !== identity2) {
120
+ row.points.offset = a.offset;
121
+ row.tint.offset = a.slotIndex * 4;
122
+ slot.slot = a.slot;
123
+ slot.blend = a.blend;
124
+ slot.page = a.page ?? "";
125
+ slot.pma = a.pma;
126
+ if (slot.kind === "region") slot.region = a.region ?? "";
127
+ row.identity = identity2;
128
+ }
129
+ slot.uvs = a.uvs;
130
+ slot.dark = a.dark ?? void 0;
131
+ slot.clip = a.clip ?? void 0;
132
+ if (slot.kind === "mesh") slot.triangles = a.triangles;
133
+ this.order[i] = slot;
134
+ }
135
+ this.order.length = count;
136
+ return this.order;
137
+ }
138
+ release() {
139
+ for (const row of this.rows) {
140
+ row.points.source = EMPTY;
141
+ row.tint.source = EMPTY;
142
+ row.slot.uvs = EMPTY;
143
+ row.slot.dark = void 0;
144
+ row.slot.clip = void 0;
145
+ if (row.slot.kind === "mesh") row.slot.triangles = EMPTY;
146
+ }
147
+ }
148
+ };
149
+ function identity(a) {
150
+ return JSON.stringify([a.slotIndex, a.slot, a.skin, a.placeholder, a.attachment, a.kind, a.page, a.region]);
151
+ }
152
+ function templateFromAttachment(a) {
153
+ const common = {
154
+ slot: a.slot,
155
+ z: a.slotIndex,
156
+ blend: a.blend,
157
+ tint: [1, 1, 1, 1]
158
+ };
159
+ return a.kind === "region" ? {
160
+ ...common,
161
+ kind: "region",
162
+ region: a.region ?? "",
163
+ corners: [],
164
+ page: a.page ?? "",
165
+ pma: a.pma,
166
+ uvs: Array.from(a.uvs)
167
+ } : {
168
+ ...common,
169
+ kind: "mesh",
170
+ page: a.page ?? "",
171
+ pma: a.pma,
172
+ vertices: [],
173
+ uvs: Array.from(a.uvs),
174
+ triangles: Array.from(a.triangles),
175
+ sequence: 0
176
+ };
177
+ }
178
+ function copyNumbers(target, source) {
179
+ if (ArrayBuffer.isView(source) && source instanceof Float64Array) {
180
+ target.set(source);
181
+ } else {
182
+ for (let i = 0; i < target.length; i++) target[i] = source[i];
183
+ }
184
+ }
185
+ function savedNumbers(source, offset, count) {
186
+ const result = new Array(count);
187
+ for (let i = 0; i < count; i++) result[i] = source[offset + i];
188
+ return result;
189
+ }
190
+ var RetainedRuntimeFrame = class {
191
+ constructor(owner = "PixiRig") {
192
+ this.owner = owner;
193
+ }
194
+ owner;
195
+ templates = /* @__PURE__ */ new WeakMap();
196
+ immutable = /* @__PURE__ */ new WeakSet();
197
+ checkedTemplates = /* @__PURE__ */ new Map();
198
+ rows = [];
199
+ vertices = new Float64Array(0);
200
+ tint = new Float64Array(0);
201
+ count = 0;
202
+ generation = 0;
203
+ retained = null;
204
+ capture(frame, seed) {
205
+ let snapshot = null;
206
+ const { count, attachments, drawOrder, vertices, tint } = frame;
207
+ if (this.vertices.length !== vertices.length) this.vertices = new Float64Array(vertices.length);
208
+ if (this.tint.length !== tint.length) this.tint = new Float64Array(tint.length);
209
+ copyNumbers(this.vertices, vertices);
210
+ copyNumbers(this.tint, tint);
211
+ for (let i = 0; i < count; i++) {
212
+ const a = attachments[drawOrder[i]];
213
+ let template = this.immutable.has(a) ? this.templates.get(a) : void 0;
214
+ if (!template) {
215
+ const key = identity(a);
216
+ template = this.checkedTemplates.get(key);
217
+ if (!template) {
218
+ if (seed) {
219
+ snapshot ??= seed();
220
+ template = snapshot.order[i];
221
+ if (!template || template.slot !== a.slot || template.kind !== a.kind || template.page !== a.page) {
222
+ throw new Error(`${this.owner}: live slot "${a.slot}" needs a matching snapshot metadata seed at draw index ${i}`);
223
+ }
224
+ } else {
225
+ template = templateFromAttachment(a);
226
+ }
227
+ if (this.checkedTemplates.size >= Math.max(64, frame.attachments.length * 2)) this.checkedTemplates.clear();
228
+ this.checkedTemplates.set(key, template);
229
+ }
230
+ if (frozenData(a)) {
231
+ this.immutable.add(a);
232
+ this.templates.set(a, template);
233
+ }
234
+ }
235
+ let row = this.rows[i];
236
+ if (!row) {
237
+ row = { template, offset: a.offset, count: a.count, tintOffset: a.slotIndex * 4 };
238
+ this.rows[i] = row;
239
+ }
240
+ row.template = template;
241
+ row.offset = a.offset;
242
+ row.count = a.count;
243
+ row.tintOffset = a.slotIndex * 4;
244
+ if (a.dark === null) row.dark = void 0;
245
+ else {
246
+ const dark = row.darkBuffer ??= [];
247
+ dark.length = a.dark.length;
248
+ for (let j = 0; j < dark.length; j++) dark[j] = a.dark[j];
249
+ row.dark = dark;
250
+ }
251
+ if (a.clip === null) row.clip = void 0;
252
+ else {
253
+ const clip = row.clipBuffer ??= { polygon: [], inverse: false, convex: true };
254
+ clip.polygon.length = a.clip.polygon.length;
255
+ for (let j = 0; j < clip.polygon.length; j++) clip.polygon[j] = a.clip.polygon[j];
256
+ clip.inverse = a.clip.inverse;
257
+ clip.convex = a.clip.convex;
258
+ row.clip = clip;
259
+ }
260
+ }
261
+ this.count = frame.count;
262
+ this.generation = frame.generation;
263
+ this.retained = null;
264
+ }
265
+ snapshot() {
266
+ if (this.retained) return this.retained;
267
+ const order = [];
268
+ for (let i = 0; i < this.count; i++) {
269
+ const { template, offset, count, tintOffset, dark, clip } = this.rows[i];
270
+ const common = { ...template, tint: savedNumbers(this.tint, tintOffset, 4) };
271
+ if (dark) common.dark = Array.from(dark);
272
+ else delete common.dark;
273
+ if (clip) common.clip = {
274
+ polygon: Array.from(clip.polygon),
275
+ inverse: clip.inverse,
276
+ convex: clip.convex
277
+ };
278
+ else delete common.clip;
279
+ 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 });
280
+ }
281
+ this.retained = { order };
282
+ Object.defineProperty(this.retained, "generation", { value: this.generation, enumerable: false, configurable: true });
283
+ return this.retained;
284
+ }
285
+ /** Explicit draw() already requested a detached snapshot. */
286
+ retain(frame) {
287
+ this.retained = frame;
288
+ }
289
+ };
290
+
1
291
  // packages/three/src/three.ts
2
292
  import { RigPlayer } from "@rig-c/play";
3
293
  import {
@@ -39,11 +329,12 @@ function hull(points) {
39
329
  };
40
330
  return [...half(sorted).slice(0, -1), ...half([...sorted].reverse()).slice(0, -1)];
41
331
  }
42
- function clipPolygon(clip, detail, capacity) {
332
+ function clipPolygon(clip, detail, capacity, indexed = false) {
43
333
  const fail = (why) => {
44
334
  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
335
  };
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");
336
+ const validPolygon = indexed ? Boolean(clip && clip.polygon && typeof clip.polygon === "object" && Number.isInteger(clip.polygon.length)) : Boolean(clip && Array.isArray(clip.polygon));
337
+ 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
338
  if (clip.polygon.length / 2 > capacity) fail(`THREE_CLIP_CAPACITY: found ${clip.polygon.length / 2} vertices`);
48
339
  const points = [];
49
340
  for (let i = 0; i < clip.polygon.length; i += 2) {
@@ -80,7 +371,7 @@ var livePages = /* @__PURE__ */ new Set();
80
371
  var getLivePageTextureCount = () => livePages.size;
81
372
  var getPageRefCount = (image) => pages.get(image)?.holders.size ?? 0;
82
373
  var getSharedPageTexture = (image) => pages.get(image)?.texture;
83
- var REGION_INDICES = [0, 1, 2, 2, 3, 0];
374
+ var REGION_INDICES = Object.freeze([0, 1, 2, 2, 3, 0]);
84
375
  var VERTEX = `
85
376
  out vec2 vUV;
86
377
  out vec2 vPosition;
@@ -137,6 +428,16 @@ var ThreeRenderer = class {
137
428
  pool = /* @__PURE__ */ new Map();
138
429
  materials = /* @__PURE__ */ new Map();
139
430
  retained = /* @__PURE__ */ new Set();
431
+ liveViews = new LiveDrawViews("ThreeRenderer");
432
+ retainedRuntimeFrame = new RetainedRuntimeFrame("ThreeRenderer");
433
+ accepted = null;
434
+ hasRetained = false;
435
+ get drawnFrame() {
436
+ if (this.accepted) return this.accepted;
437
+ if (!this.hasRetained) return null;
438
+ this.accepted = this.retainedRuntimeFrame.snapshot();
439
+ return this.accepted;
440
+ }
140
441
  get geometryUploadCount() {
141
442
  return this.positionUploadCount + this.uvUploadCount + this.triangleUploadCount;
142
443
  }
@@ -150,7 +451,7 @@ var ThreeRenderer = class {
150
451
  const image = this.assets.pageImages.get(name);
151
452
  return image && this.retained.has(image) ? pages.get(image)?.texture : void 0;
152
453
  }
153
- prepare(slot) {
454
+ prepare(slot, key, indexed) {
154
455
  const detail = `ThreeRenderer: slot "${slot?.slot ?? "<unnamed>"}" on page "${slot?.page ?? "<unnamed>"}"`;
155
456
  const fail = (why) => {
156
457
  throw new Error(`${detail}: ${why}`);
@@ -159,26 +460,58 @@ var ThreeRenderer = class {
159
460
  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
461
  if (!["Normal", "Additive", "Multiply", "Screen"].includes(slot.blend)) fail(`blend=${slot.blend} (expected Normal, Additive, Multiply, Screen)`);
161
462
  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)`);
463
+ if (!indexed && !Number.isFinite(slot.z)) fail(`z=${String(slot.z)} (expected finite draw-order index)`);
163
464
  const image = this.assets.pageImages.get(slot.page);
164
465
  if (!image || !(image.naturalWidth > 0 && image.naturalHeight > 0)) fail("page image absent or not decoded (expected positive naturalWidth/naturalHeight)");
165
466
  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)`);
467
+ const source = numberSource(values), offset = numberOffset(values), length = numberCount(values);
468
+ if (!indexed && !Array.isArray(values) || length !== count) fail(`${field} length=${Array.isArray(values) || indexed ? length : void 0} (expected ${count})`);
469
+ for (let i = 0; i < count; i++) {
470
+ const value = source[offset + i];
471
+ if (!Number.isFinite(value) || !Number.isFinite(Math.fround(value))) fail(`${field}[${i}]=${String(value)} (expected finite float32)`);
472
+ }
168
473
  };
169
474
  valid(slot.tint, 4, "tint");
170
475
  if (slot.dark !== void 0) valid(slot.dark, 3, "dark");
171
476
  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");
477
+ const coordCount = numberCount(coords);
478
+ if (!indexed && !Array.isArray(coords) || coordCount < 6 || coordCount % 2 !== 0) fail(`positions length=${Array.isArray(coords) || indexed ? coordCount : void 0} (expected even length >=6)`);
479
+ valid(coords, slot.kind === "region" ? 8 : coordCount, slot.kind === "region" ? "corners" : "vertices");
480
+ const prior = this.pool.get(key);
481
+ const priorPositions = prior?.mesh.geometry.getAttribute("position");
482
+ const sameVertexCount = !this.alwaysReuploadGeometry && prior !== void 0 && priorPositions !== void 0 && priorPositions.array.length === coordCount / 2 * 3;
483
+ if (!sameVertexCount || !isImmutableSpan(slot.uvs, indexed) || prior.uvs !== slot.uvs) {
484
+ valid(slot.uvs, coordCount, "uvs");
485
+ }
175
486
  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 };
487
+ if (!sameVertexCount || !isImmutableSpan(indices, indexed) || prior.indices !== indices) {
488
+ if (!indexed && !Array.isArray(indices) || indices.length < 3 || indices.length % 3 !== 0) fail(`triangles length=${indices?.length} (expected multiple of three >=3)`);
489
+ for (let i = 0; i < indices.length; i++) {
490
+ const v = indices[i];
491
+ if (!Number.isInteger(v) || v < 0 || v >= coordCount / 2) fail(`triangles[${i}]=${v} (expected integer 0..${coordCount / 2 - 1})`);
492
+ }
493
+ }
494
+ if (!indexed && slot.kind === "mesh" && !Number.isInteger(slot.sequence)) fail(`sequence=${slot.sequence} (expected integer)`);
495
+ const tintSource = numberSource(slot.tint), tintOffset = numberOffset(slot.tint);
496
+ const light = [
497
+ tintSource[tintOffset + 0],
498
+ tintSource[tintOffset + 1],
499
+ tintSource[tintOffset + 2],
500
+ tintSource[tintOffset + 3]
501
+ ];
502
+ const dark = slot.dark ? [slot.dark[0], slot.dark[1], slot.dark[2]] : [0, 0, 0];
503
+ return {
504
+ slot,
505
+ key,
506
+ image,
507
+ positions: coords,
508
+ indices,
509
+ polygon: slot.clip !== void 0 ? clipPolygon(slot.clip, detail, this.host.capabilities.maxTextureSize, indexed) : null,
510
+ light,
511
+ dark,
512
+ inverse: slot.clip?.inverse ?? false,
513
+ blend: slot.blend
514
+ };
182
515
  }
183
516
  material(item) {
184
517
  let material = this.materials.get(item.image);
@@ -216,71 +549,162 @@ var ThreeRenderer = class {
216
549
  }
217
550
  render(frame) {
218
551
  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));
552
+ if (!frame || typeof frame !== "object") throw new Error("ThreeRenderer: frame.order absent (expected PosedSlot array)");
553
+ if ("order" in frame) {
554
+ if (!Array.isArray(frame.order)) throw new Error("ThreeRenderer: frame.order absent (expected PosedSlot array)");
555
+ this.renderOrder(frame.order, false);
556
+ this.accepted = frame;
557
+ this.hasRetained = false;
558
+ return;
559
+ }
560
+ try {
561
+ this.renderOrder(this.liveViews.read(frame), true);
562
+ this.retainedRuntimeFrame.capture(frame);
563
+ this.accepted = null;
564
+ this.hasRetained = true;
565
+ } finally {
566
+ this.liveViews.release();
567
+ }
568
+ }
569
+ renderOrder(order, indexed) {
570
+ const occurrences = /* @__PURE__ */ new Map();
571
+ const prepared = order.map((s) => {
572
+ const slotName = typeof s?.slot === "string" && s.slot.length > 0 ? s.slot : "<unnamed>";
573
+ const occurrence = occurrences.get(slotName) ?? 0;
574
+ occurrences.set(slotName, occurrence + 1);
575
+ return this.prepare(s, `${slotName}\0${occurrence}`, indexed);
576
+ });
221
577
  const conventions = /* @__PURE__ */ new Map();
222
578
  for (const item of prepared) {
223
579
  const pma = conventions.get(item.image) ?? pages.get(item.image)?.pma;
224
580
  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
581
  conventions.set(item.image, item.slot.pma);
226
582
  }
227
- const occurrences = /* @__PURE__ */ new Map();
228
583
  const active = [];
229
584
  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}`;
585
+ const { key } = item;
233
586
  let entry = this.pool.get(key);
234
587
  if (!entry) {
235
588
  const geometry = new BufferGeometry();
236
589
  const mesh2 = new Mesh(geometry, this.material(item));
237
590
  mesh2.frustumCulled = false;
238
591
  mesh2.name = item.slot.slot;
239
- entry = { mesh: mesh2, polygon: null, polygonValues: [], uvs: null, indices: null };
592
+ entry = {
593
+ mesh: mesh2,
594
+ polygon: null,
595
+ polygonValues: [],
596
+ uvs: null,
597
+ indices: null,
598
+ light: item.light,
599
+ dark: item.dark,
600
+ inverse: item.inverse,
601
+ blend: item.blend,
602
+ needsWideIndex: false
603
+ };
604
+ const current = entry;
605
+ mesh2.onBeforeRender = () => {
606
+ const material = mesh2.material;
607
+ material.uniforms.light.value = current.light;
608
+ material.uniforms.dark.value = current.dark;
609
+ material.uniforms.polygon.value = current.polygon ?? material.uniforms.page.value;
610
+ material.uniforms.points.value = current.polygon === null ? 0 : current.polygonValues.length / 2;
611
+ material.uniforms.inverse.value = current.inverse;
612
+ material.blendEquation = material.blendEquationAlpha = AddEquation;
613
+ material.blendSrc = current.blend === "Multiply" ? DstColorFactor : OneFactor;
614
+ material.blendDst = current.blend === "Additive" ? OneFactor : current.blend === "Screen" ? OneMinusSrcColorFactor : OneMinusSrcAlphaFactor;
615
+ material.blendSrcAlpha = OneFactor;
616
+ material.blendDstAlpha = material.blendDst;
617
+ material.uniformsNeedUpdate = true;
618
+ };
240
619
  this.pool.set(key, entry);
241
620
  }
242
621
  const { mesh } = entry;
622
+ const positionCount = numberCount(item.positions);
243
623
  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))) {
624
+ let needsWideIndex = entry.needsWideIndex;
625
+ if (this.alwaysReuploadGeometry || entry.indices !== item.indices || !oldIndex) {
626
+ needsWideIndex = false;
627
+ if (positionCount / 2 > 65535) {
628
+ for (let i = 0; i < item.indices.length; i++) if (item.indices[i] >= 65535) {
629
+ needsWideIndex = true;
630
+ break;
631
+ }
632
+ }
633
+ entry.needsWideIndex = needsWideIndex;
634
+ }
635
+ 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
636
  mesh.geometry.dispose();
247
637
  mesh.geometry = new BufferGeometry();
248
638
  entry.uvs = entry.indices = null;
249
639
  }
250
- mesh.posedSlot = item.slot;
640
+ entry.light = item.light;
641
+ entry.dark = item.dark;
642
+ entry.inverse = item.inverse;
643
+ entry.blend = item.blend;
644
+ if (!indexed) {
645
+ mesh.posedSlot = item.slot;
646
+ } else {
647
+ const prior = mesh.posedSlot;
648
+ 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) {
649
+ 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 };
650
+ }
651
+ }
251
652
  mesh.material = this.material(item);
252
653
  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);
654
+ const posLength = positionCount / 2 * 3;
655
+ let posAttr = mesh.geometry.getAttribute("position");
656
+ let posChanged = this.alwaysReuploadGeometry || !posAttr || posAttr.array.length !== posLength;
657
+ if (!posAttr || posAttr.array.length !== posLength) {
658
+ posAttr = new BufferAttribute(new Float32Array(posLength), 3).setUsage(DynamicDrawUsage);
659
+ mesh.geometry.setAttribute("position", posAttr);
660
+ }
661
+ const posArray = posAttr.array;
662
+ const posSource = numberSource(item.positions), posOffset = numberOffset(item.positions);
663
+ for (let i = 0, out = 0; i < positionCount; i += 2, out += 3) {
664
+ const x = Math.fround(posSource[posOffset + i]), y = Math.fround(posSource[posOffset + i + 1]);
665
+ if (posArray[out] !== x || posArray[out + 1] !== y || posArray[out + 2] !== 0) {
666
+ posArray[out] = x;
667
+ posArray[out + 1] = y;
668
+ posArray[out + 2] = 0;
669
+ posChanged = true;
670
+ }
671
+ }
672
+ if (posChanged) {
673
+ posAttr.needsUpdate = true;
674
+ this.positionUploadCount++;
675
+ }
676
+ if (this.alwaysReuploadGeometry || entry.uvs !== item.slot.uvs) {
677
+ const uvs = item.slot.uvs;
678
+ let uvAttr = mesh.geometry.getAttribute("uv");
679
+ let uvChanged = true;
680
+ if (!uvAttr || uvAttr.array.length !== uvs.length) {
681
+ uvAttr = new BufferAttribute(new Float32Array(uvs.length), 2).setUsage(DynamicDrawUsage);
682
+ mesh.geometry.setAttribute("uv", uvAttr);
259
683
  }
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;
684
+ const uvArray = uvAttr.array;
685
+ for (let i = 0; i < uvs.length; i++) {
686
+ const v = Math.fround(uvs[i]);
687
+ if (uvArray[i] !== v) {
688
+ uvArray[i] = v;
689
+ uvChanged = true;
690
+ }
264
691
  }
265
- if (changed) {
266
- attribute.needsUpdate = true;
267
- if (name === "position") this.positionUploadCount++;
268
- else this.uvUploadCount++;
692
+ if (uvChanged) {
693
+ uvAttr.needsUpdate = true;
694
+ this.uvUploadCount++;
269
695
  }
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;
696
+ entry.uvs = uvs;
277
697
  }
278
698
  if (this.alwaysReuploadGeometry || entry.indices !== item.indices) {
279
699
  const before = mesh.geometry.getIndex();
280
700
  if (before && before.count === item.indices.length) {
281
701
  for (let i = 0; i < item.indices.length; i++) before.setX(i, item.indices[i]);
282
702
  before.needsUpdate = true;
283
- } else mesh.geometry.setIndex([...item.indices]);
703
+ } else {
704
+ const copied = new Array(item.indices.length);
705
+ for (let i = 0; i < item.indices.length; i++) copied[i] = item.indices[i];
706
+ mesh.geometry.setIndex(copied);
707
+ }
284
708
  this.triangleUploadCount++;
285
709
  entry.indices = item.indices;
286
710
  }
@@ -300,21 +724,6 @@ var ThreeRenderer = class {
300
724
  entry.polygon = null;
301
725
  entry.polygonValues = [];
302
726
  }
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
727
  active.push(mesh);
319
728
  });
320
729
  for (const entry of this.pool.values()) if (entry.mesh.parent === this.root) this.root.remove(entry.mesh);
@@ -361,7 +770,13 @@ var ThreeRig = class extends Group {
361
770
  if (!assets?.document || !(assets.pageImages instanceof Map) || !isWebGLHost(options?.renderer)) throw new Error("ThreeRig: expected ModelAssets and options.renderer WebGLRenderer");
362
771
  const scale = options.timescale ?? options.timeScale ?? 1;
363
772
  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 });
773
+ this.player = new RigPlayer(document.createElement("div"), assets, {
774
+ mode: assets.regions === "none" ? "canvas" : "dom",
775
+ skin: options.skin,
776
+ slots: options.slots,
777
+ liveFrameChecks: options.liveFrameChecks,
778
+ poser: options.poser
779
+ });
365
780
  this.renderer = new ThreeRenderer(this, assets, options.renderer);
366
781
  this.timescale = scale;
367
782
  try {
@@ -375,7 +790,7 @@ var ThreeRig = class extends Group {
375
790
  assets;
376
791
  player;
377
792
  renderer;
378
- lastFrame;
793
+ lastFrame = null;
379
794
  destroyed = false;
380
795
  active() {
381
796
  if (this.destroyed) throw new Error("ThreeRig: called after destroy (expected active rig)");
@@ -383,20 +798,20 @@ var ThreeRig = class extends Group {
383
798
  play(name, options = {}) {
384
799
  this.active();
385
800
  this.player.play(name, options);
386
- this.draw();
801
+ this.drawCurrent();
387
802
  }
388
803
  seek(time) {
389
804
  this.active();
390
805
  if (!Number.isFinite(time)) throw new Error(`ThreeRig: time=${time} (expected finite seconds)`);
391
806
  this.player.seek(time);
392
- this.draw();
807
+ this.drawCurrent();
393
808
  }
394
809
  advance(dt) {
395
810
  this.active();
396
811
  if (!Number.isFinite(dt) || dt < 0) throw new Error(`ThreeRig: dt=${dt} (expected finite seconds >=0)`);
397
812
  if (this.timescale === 0) return;
398
813
  this.player.advance(dt);
399
- this.draw();
814
+ this.drawCurrent();
400
815
  }
401
816
  update(dt) {
402
817
  this.advance(dt);
@@ -408,9 +823,29 @@ var ThreeRig = class extends Group {
408
823
  this.lastFrame = frame;
409
824
  return frame;
410
825
  }
826
+ renderFrame(frame) {
827
+ this.active();
828
+ if ("order" in frame) {
829
+ this.renderer.render(frame);
830
+ this.lastFrame = frame;
831
+ return;
832
+ }
833
+ if (frame.generation !== this.player.generation) {
834
+ throw new Error(`ThreeRig: live frame generation ${frame.generation} needs current player generation ${this.player.generation}`);
835
+ }
836
+ if (this.player.poser !== "runtime") {
837
+ this.renderFrame(this.player.posedFrame());
838
+ return;
839
+ }
840
+ this.renderer.render(frame);
841
+ this.lastFrame = null;
842
+ }
843
+ drawCurrent() {
844
+ this.renderFrame(this.player.poser === "runtime" ? this.player.liveFrame() : this.player.posedFrame());
845
+ }
411
846
  drawnFrame() {
412
847
  this.active();
413
- return this.lastFrame;
848
+ return this.lastFrame ??= this.renderer.drawnFrame;
414
849
  }
415
850
  posedFrame(slots) {
416
851
  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.3",
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:*",