@doki-land/live2d-renderer 0.0.18 → 0.0.20
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 +63 -7
- package/dist/index.js +985 -339
- package/package.json +2 -2
- package/src/backends/canvas2d.ts +6 -7
- package/src/backends/webgl2.ts +54 -33
- package/src/backends/webgpu-mesh-cache.ts +206 -0
- package/src/backends/webgpu.ts +158 -186
- package/src/cpu/evaluate.ts +63 -4
- package/src/index.ts +9 -0
- package/src/moc/moc2-to-program.ts +89 -3
- package/src/moc/moc2.ts +151 -75
- package/src/moc/moc3-to-program.ts +193 -3
- package/src/moc/moc3.ts +160 -81
- package/src/render/clipping-plan.ts +140 -0
- package/src/render/clipping.ts +22 -6
- package/src/render/mesh-interleave.ts +28 -0
- package/src/runtime/model-runtime.ts +6 -0
- package/src/types.ts +1 -1
package/src/moc/moc3.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { detectModelSettingsFormat } from "@doki-land/live2d-core";
|
|
|
10
10
|
import { isCpuProgramBytes, parseCpuProgram } from "../cpu/cpu-program.js";
|
|
11
11
|
import {
|
|
12
12
|
createModelInstance,
|
|
13
|
-
|
|
13
|
+
evaluateFrameInto,
|
|
14
14
|
setParameterValue,
|
|
15
15
|
} from "../cpu/evaluate.js";
|
|
16
16
|
import type {
|
|
@@ -21,13 +21,34 @@ import type {
|
|
|
21
21
|
import type { BlendMode, DrawableMesh } from "../types.js";
|
|
22
22
|
import { cascadedPartOpacity, readMoc3PartTables } from "./moc3-parts.js";
|
|
23
23
|
import { type Moc3Document, parseMoc3Document } from "./moc3-reader.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
evaluateMoc3PoseInto,
|
|
26
|
+
moc3ArtMeshIndicesInProgramOrder,
|
|
27
|
+
moc3DocumentToProgram,
|
|
28
|
+
} from "./moc3-to-program.js";
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
interface Moc3State {
|
|
31
|
+
/** Present for binary MOC3 input; null for cpu-program fixtures. */
|
|
32
|
+
doc: Moc3Document | null;
|
|
33
|
+
instance: ModelInstance;
|
|
34
|
+
meshes: DrawableMesh[];
|
|
35
|
+
drawView: DrawableMesh[];
|
|
36
|
+
/** art-mesh index → mesh (binary path); empty for cpu-program. */
|
|
37
|
+
byArtMesh: Map<number, DrawableMesh>;
|
|
38
|
+
poseOpacity: Float32Array;
|
|
39
|
+
poseDirty: boolean;
|
|
40
|
+
partOpacity: Map<string, number>;
|
|
41
|
+
bindings: ParameterBinding[];
|
|
42
|
+
paramIndexById: Map<string, number>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const stateByModel = new WeakMap<InternalModel, Moc3State>();
|
|
46
|
+
|
|
47
|
+
function allocateMeshesFromProgram(program: ModelProgram): DrawableMesh[] {
|
|
48
|
+
return program.drawables.map((d) => ({
|
|
28
49
|
index: d.index,
|
|
29
50
|
textureIndex: d.textureIndex,
|
|
30
|
-
vertexPositions: d.positions,
|
|
51
|
+
vertexPositions: new Float32Array(d.positions),
|
|
31
52
|
uvs: d.uvs,
|
|
32
53
|
indices: d.indices,
|
|
33
54
|
opacity: d.opacity,
|
|
@@ -37,49 +58,96 @@ function toDrawableMesh(d: FrameDrawable): DrawableMesh {
|
|
|
37
58
|
dynamicFlag: true,
|
|
38
59
|
maskIndices: [...d.maskIndices],
|
|
39
60
|
visible: d.visible,
|
|
40
|
-
};
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function buildBindings(instance: ModelInstance): {
|
|
65
|
+
bindings: ParameterBinding[];
|
|
66
|
+
paramIndexById: Map<string, number>;
|
|
67
|
+
} {
|
|
68
|
+
const bindings: ParameterBinding[] = [];
|
|
69
|
+
const paramIndexById = new Map<string, number>();
|
|
70
|
+
for (let i = 0; i < instance.program.parameters.length; i++) {
|
|
71
|
+
const p = instance.program.parameters[i]!;
|
|
72
|
+
bindings.push({
|
|
73
|
+
id: p.id,
|
|
74
|
+
min: p.min,
|
|
75
|
+
max: p.max,
|
|
76
|
+
defaultValue: p.defaultValue,
|
|
77
|
+
value: instance.parameterValues[i] ?? p.defaultValue,
|
|
78
|
+
});
|
|
79
|
+
paramIndexById.set(p.id, i);
|
|
80
|
+
}
|
|
81
|
+
return { bindings, paramIndexById };
|
|
41
82
|
}
|
|
42
83
|
|
|
43
|
-
function
|
|
44
|
-
let
|
|
45
|
-
|
|
46
|
-
|
|
84
|
+
function syncBindingValues(state: Moc3State): void {
|
|
85
|
+
for (let i = 0; i < state.bindings.length; i++) {
|
|
86
|
+
const b = state.bindings[i]!;
|
|
87
|
+
(b as { value: number }).value =
|
|
88
|
+
state.instance.parameterValues[i] ?? b.defaultValue;
|
|
47
89
|
}
|
|
48
|
-
return s;
|
|
49
90
|
}
|
|
50
91
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
bakedFingerprint: string;
|
|
57
|
-
/** Runtime PartOpacity overrides (id → 0..1). */
|
|
58
|
-
partOpacity: Map<string, number>;
|
|
92
|
+
function refreshDrawView(state: Moc3State): void {
|
|
93
|
+
const view = state.drawView;
|
|
94
|
+
view.length = 0;
|
|
95
|
+
for (const m of state.meshes) view.push(m);
|
|
96
|
+
view.sort((a, b) => a.renderOrder - b.renderOrder || a.index - b.index);
|
|
59
97
|
}
|
|
60
98
|
|
|
61
|
-
|
|
99
|
+
function applyPartOpacity(state: Moc3State): void {
|
|
100
|
+
const tables = state.doc ? readMoc3PartTables(state.doc) : null;
|
|
101
|
+
for (const mesh of state.meshes) {
|
|
102
|
+
const base = state.poseOpacity[mesh.index] ?? mesh.opacity;
|
|
103
|
+
if (!tables || state.partOpacity.size === 0) {
|
|
104
|
+
mesh.opacity = base;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
mesh.opacity =
|
|
108
|
+
base * cascadedPartOpacity(tables, mesh.index, state.partOpacity);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
62
111
|
|
|
63
112
|
function bakePose(state: Moc3State): void {
|
|
64
|
-
if (!state.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
if (!state.poseDirty) return;
|
|
114
|
+
if (state.doc) {
|
|
115
|
+
const values = state.instance.parameterValues;
|
|
116
|
+
evaluateMoc3PoseInto(
|
|
117
|
+
state.doc,
|
|
118
|
+
(i) => values[i] ?? 0,
|
|
119
|
+
state.byArtMesh,
|
|
120
|
+
state.poseOpacity,
|
|
121
|
+
);
|
|
122
|
+
} else {
|
|
123
|
+
evaluateFrameInto(state.instance, state.meshes, state.poseOpacity);
|
|
124
|
+
}
|
|
125
|
+
refreshDrawView(state);
|
|
126
|
+
syncBindingValues(state);
|
|
127
|
+
state.poseDirty = false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function frameFromMeshes(
|
|
131
|
+
meshes: readonly DrawableMesh[],
|
|
132
|
+
timeSeconds: number,
|
|
133
|
+
): FrameSnapshot {
|
|
134
|
+
const drawables: FrameDrawable[] = [];
|
|
135
|
+
for (const m of meshes) {
|
|
136
|
+
drawables.push({
|
|
137
|
+
index: m.index,
|
|
138
|
+
textureIndex: m.textureIndex,
|
|
139
|
+
positions: m.vertexPositions,
|
|
140
|
+
uvs: m.uvs,
|
|
141
|
+
indices: m.indices,
|
|
142
|
+
opacity: m.opacity,
|
|
143
|
+
blendMode: m.blendMode,
|
|
144
|
+
renderOrder: m.renderOrder,
|
|
145
|
+
visible: m.visible,
|
|
146
|
+
invertedMask: m.invertedMask,
|
|
147
|
+
maskIndices: m.maskIndices,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return { timeSeconds, drawables };
|
|
83
151
|
}
|
|
84
152
|
|
|
85
153
|
/** moc3 model backend for binary `.moc3` input or CPU `.program.json` fixtures. */
|
|
@@ -121,18 +189,39 @@ export class Moc3Backend implements ModelBackend {
|
|
|
121
189
|
}
|
|
122
190
|
|
|
123
191
|
const instance = createModelInstance(program);
|
|
192
|
+
const meshes = allocateMeshesFromProgram(program);
|
|
193
|
+
const byArtMesh = new Map<number, DrawableMesh>();
|
|
194
|
+
if (doc) {
|
|
195
|
+
const artOrder = moc3ArtMeshIndicesInProgramOrder(doc);
|
|
196
|
+
for (let i = 0; i < artOrder.length; i++) {
|
|
197
|
+
const art = artOrder[i]!;
|
|
198
|
+
const mesh = meshes[i];
|
|
199
|
+
if (mesh) byArtMesh.set(art, mesh);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const poseOpacity = new Float32Array(meshes.length);
|
|
203
|
+
for (const m of meshes) poseOpacity[m.index] = m.opacity;
|
|
204
|
+
const { bindings, paramIndexById } = buildBindings(instance);
|
|
124
205
|
const model: InternalModel = {
|
|
125
206
|
id: settings.name ?? settings.url,
|
|
126
207
|
settings,
|
|
127
208
|
format: "moc3",
|
|
128
209
|
};
|
|
129
|
-
|
|
210
|
+
const state: Moc3State = {
|
|
130
211
|
doc,
|
|
131
212
|
instance,
|
|
132
|
-
|
|
133
|
-
|
|
213
|
+
meshes,
|
|
214
|
+
drawView: [],
|
|
215
|
+
byArtMesh,
|
|
216
|
+
poseOpacity,
|
|
217
|
+
poseDirty: true,
|
|
134
218
|
partOpacity: new Map(),
|
|
135
|
-
|
|
219
|
+
bindings,
|
|
220
|
+
paramIndexById,
|
|
221
|
+
};
|
|
222
|
+
bakePose(state);
|
|
223
|
+
applyPartOpacity(state);
|
|
224
|
+
stateByModel.set(model, state);
|
|
136
225
|
return model;
|
|
137
226
|
}
|
|
138
227
|
|
|
@@ -141,54 +230,39 @@ export class Moc3Backend implements ModelBackend {
|
|
|
141
230
|
if (!state) return;
|
|
142
231
|
state.instance.timeSeconds += deltaTimeSeconds;
|
|
143
232
|
bakePose(state);
|
|
144
|
-
|
|
145
|
-
state.lastFrame = {
|
|
146
|
-
...state.lastFrame,
|
|
147
|
-
timeSeconds: state.instance.timeSeconds,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
233
|
+
applyPartOpacity(state);
|
|
150
234
|
}
|
|
151
235
|
|
|
152
236
|
getDrawables(model: InternalModel): DrawableMesh[] {
|
|
153
237
|
const state = stateByModel.get(model);
|
|
154
238
|
if (!state) return [];
|
|
155
239
|
bakePose(state);
|
|
156
|
-
|
|
157
|
-
state.
|
|
158
|
-
const tables = state.doc ? readMoc3PartTables(state.doc) : null;
|
|
159
|
-
return frame.drawables.map((d) => {
|
|
160
|
-
const mesh = toDrawableMesh(d);
|
|
161
|
-
if (!tables || state.partOpacity.size === 0) return mesh;
|
|
162
|
-
const mul = cascadedPartOpacity(tables, d.index, state.partOpacity);
|
|
163
|
-
return { ...mesh, opacity: mesh.opacity * mul };
|
|
164
|
-
});
|
|
240
|
+
applyPartOpacity(state);
|
|
241
|
+
return state.drawView;
|
|
165
242
|
}
|
|
166
243
|
|
|
167
244
|
captureFrame(model: InternalModel): FrameSnapshot | null {
|
|
168
245
|
const state = stateByModel.get(model);
|
|
169
246
|
if (!state) return null;
|
|
170
247
|
bakePose(state);
|
|
171
|
-
|
|
172
|
-
state.
|
|
173
|
-
const tables = state.doc ? readMoc3PartTables(state.doc) : null;
|
|
174
|
-
if (!tables || state.partOpacity.size === 0) return frame;
|
|
175
|
-
return {
|
|
176
|
-
...frame,
|
|
177
|
-
drawables: frame.drawables.map((d) => ({
|
|
178
|
-
...d,
|
|
179
|
-
opacity:
|
|
180
|
-
d.opacity *
|
|
181
|
-
cascadedPartOpacity(tables, d.index, state.partOpacity),
|
|
182
|
-
})),
|
|
183
|
-
};
|
|
248
|
+
applyPartOpacity(state);
|
|
249
|
+
return frameFromMeshes(state.drawView, state.instance.timeSeconds);
|
|
184
250
|
}
|
|
185
251
|
|
|
186
252
|
setParameter(model: InternalModel, id: string, value: number): void {
|
|
187
253
|
const state = stateByModel.get(model);
|
|
188
254
|
if (!state) return;
|
|
189
255
|
setParameterValue(state.instance, id, value);
|
|
190
|
-
|
|
191
|
-
|
|
256
|
+
const index = state.paramIndexById.get(id);
|
|
257
|
+
if (index !== undefined) {
|
|
258
|
+
const binding = state.bindings[index];
|
|
259
|
+
if (binding) {
|
|
260
|
+
(binding as { value: number }).value =
|
|
261
|
+
state.instance.parameterValues[index] ??
|
|
262
|
+
binding.defaultValue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
state.poseDirty = true;
|
|
192
266
|
}
|
|
193
267
|
|
|
194
268
|
setPartOpacity(model: InternalModel, id: string, value: number): void {
|
|
@@ -196,19 +270,20 @@ export class Moc3Backend implements ModelBackend {
|
|
|
196
270
|
if (!state) return;
|
|
197
271
|
const v = Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : 1;
|
|
198
272
|
state.partOpacity.set(id, v);
|
|
199
|
-
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
resolveParameter(model: InternalModel, id: string): number | undefined {
|
|
276
|
+
const state = stateByModel.get(model);
|
|
277
|
+
if (!state) return undefined;
|
|
278
|
+
return state.paramIndexById.get(id);
|
|
200
279
|
}
|
|
201
280
|
|
|
202
281
|
listParameters(model: InternalModel): readonly ParameterBinding[] {
|
|
203
282
|
const state = stateByModel.get(model);
|
|
204
283
|
if (!state) return [];
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
max: p.max,
|
|
209
|
-
defaultValue: p.defaultValue,
|
|
210
|
-
value: state.instance.parameterValues[i] ?? p.defaultValue,
|
|
211
|
-
}));
|
|
284
|
+
bakePose(state);
|
|
285
|
+
syncBindingValues(state);
|
|
286
|
+
return state.bindings;
|
|
212
287
|
}
|
|
213
288
|
|
|
214
289
|
hitTest(_model: InternalModel, _x: number, _y: number): string | null {
|
|
@@ -218,6 +293,10 @@ export class Moc3Backend implements ModelBackend {
|
|
|
218
293
|
destroyModel(model: InternalModel): void {
|
|
219
294
|
stateByModel.delete(model);
|
|
220
295
|
}
|
|
296
|
+
|
|
297
|
+
getResidentInstance(model: InternalModel): ModelInstance | null {
|
|
298
|
+
return stateByModel.get(model)?.instance ?? null;
|
|
299
|
+
}
|
|
221
300
|
}
|
|
222
301
|
|
|
223
302
|
export function createMoc3Backend(): Moc3Backend {
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resident clipping / mask atlas plan.
|
|
3
|
+
*
|
|
4
|
+
* Topology (maskIndices / invert / packing options) is compiled once; per frame
|
|
5
|
+
* only `modelBounds` is refreshed from current vertex positions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DrawableMesh } from "../types.js";
|
|
9
|
+
import {
|
|
10
|
+
type ClippingDrawableRef,
|
|
11
|
+
type ClippingPartition,
|
|
12
|
+
calcClippedDrawableBounds,
|
|
13
|
+
type LaidOutClippingContext,
|
|
14
|
+
type MaskAtlasOptions,
|
|
15
|
+
type MaskBoundsMeshRef,
|
|
16
|
+
type MaskLayoutRect,
|
|
17
|
+
partitionForClipping,
|
|
18
|
+
} from "./clipping.js";
|
|
19
|
+
|
|
20
|
+
/** Stable key for mask topology (ignores vertex positions / opacity). */
|
|
21
|
+
export function clippingTopologyKey(
|
|
22
|
+
drawables: readonly ClippingDrawableRef[],
|
|
23
|
+
): string {
|
|
24
|
+
let key = `n:${drawables.length}`;
|
|
25
|
+
for (let i = 0; i < drawables.length; i++) {
|
|
26
|
+
const d = drawables[i]!;
|
|
27
|
+
key += `|${d.index}:${d.invertedMask ? 1 : 0}:`;
|
|
28
|
+
const masks = d.maskIndices;
|
|
29
|
+
for (let j = 0; j < masks.length; j++) {
|
|
30
|
+
key += `${masks[j]!},`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return key;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function atlasOptionsKey(options: MaskAtlasOptions): string {
|
|
37
|
+
return `${options.mode ?? "rgba"}:${options.inset ?? ""}:${options.renderTextureCount ?? ""}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type ResidentContext = {
|
|
41
|
+
-readonly [K in keyof LaidOutClippingContext]: LaidOutClippingContext[K];
|
|
42
|
+
} & { modelBounds: MaskLayoutRect };
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Update `modelBounds` on resident contexts without reallocating the plan.
|
|
46
|
+
*/
|
|
47
|
+
export function fitClippingContextsInPlace(
|
|
48
|
+
contexts: readonly ResidentContext[],
|
|
49
|
+
byIndex: ReadonlyMap<number, MaskBoundsMeshRef>,
|
|
50
|
+
margin = 0.05,
|
|
51
|
+
): void {
|
|
52
|
+
for (let i = 0; i < contexts.length; i++) {
|
|
53
|
+
const ctx = contexts[i]!;
|
|
54
|
+
ctx.modelBounds = calcClippedDrawableBounds(
|
|
55
|
+
ctx.clippedIndices,
|
|
56
|
+
byIndex,
|
|
57
|
+
margin,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Caches atlas layout + mask/clipped sets while drawable mask topology is
|
|
64
|
+
* unchanged. Call {@link resolve} each frame; call {@link clear} on destroy.
|
|
65
|
+
*/
|
|
66
|
+
export class ResidentClippingPlan {
|
|
67
|
+
#cacheKey = "";
|
|
68
|
+
#contexts: ResidentContext[] = [];
|
|
69
|
+
#clipped: Set<number> = new Set();
|
|
70
|
+
#maskOnly: Set<number> = new Set();
|
|
71
|
+
readonly #byIndex = new Map<number, MaskBoundsMeshRef>();
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns a partition whose `contexts` array identity is stable across
|
|
75
|
+
* frames with the same topology. `modelBounds` are refreshed in place.
|
|
76
|
+
*/
|
|
77
|
+
resolve(
|
|
78
|
+
drawables: readonly ClippingDrawableRef[],
|
|
79
|
+
options: MaskAtlasOptions = {},
|
|
80
|
+
): ClippingPartition {
|
|
81
|
+
const nextKey = `${clippingTopologyKey(drawables)}|${atlasOptionsKey(options)}`;
|
|
82
|
+
if (nextKey !== this.#cacheKey) {
|
|
83
|
+
const partitioned = partitionForClipping(drawables, options);
|
|
84
|
+
this.#contexts = partitioned.contexts.map((ctx) => ({
|
|
85
|
+
key: ctx.key,
|
|
86
|
+
maskIndices: ctx.maskIndices,
|
|
87
|
+
clippedIndices: ctx.clippedIndices,
|
|
88
|
+
invertedMask: ctx.invertedMask,
|
|
89
|
+
layout: ctx.layout,
|
|
90
|
+
channelIndex: ctx.channelIndex,
|
|
91
|
+
channelFlag: ctx.channelFlag,
|
|
92
|
+
bufferIndex: ctx.bufferIndex,
|
|
93
|
+
modelBounds: { ...ctx.modelBounds },
|
|
94
|
+
}));
|
|
95
|
+
this.#clipped = new Set(partitioned.clipped);
|
|
96
|
+
this.#maskOnly = new Set(partitioned.maskOnly);
|
|
97
|
+
this.#cacheKey = nextKey;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.#byIndex.clear();
|
|
101
|
+
for (let i = 0; i < drawables.length; i++) {
|
|
102
|
+
const d = drawables[i]!;
|
|
103
|
+
if ("vertexPositions" in d) {
|
|
104
|
+
this.#byIndex.set(d.index, d as MaskBoundsMeshRef);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
fitClippingContextsInPlace(this.#contexts, this.#byIndex);
|
|
108
|
+
return {
|
|
109
|
+
contexts: this.#contexts,
|
|
110
|
+
clipped: this.#clipped,
|
|
111
|
+
maskOnly: this.#maskOnly,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Convenience for full drawable meshes (GPU backends). */
|
|
116
|
+
resolveMeshes(
|
|
117
|
+
drawables: readonly DrawableMesh[],
|
|
118
|
+
options: MaskAtlasOptions = {},
|
|
119
|
+
): ClippingPartition {
|
|
120
|
+
return this.resolve(drawables, options);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** True when the last {@link resolve} reused the cached atlas layout. */
|
|
124
|
+
get hasPlan(): boolean {
|
|
125
|
+
return this.#cacheKey.length > 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Exposed for tests — context array identity while topology holds. */
|
|
129
|
+
get residentContexts(): readonly LaidOutClippingContext[] {
|
|
130
|
+
return this.#contexts;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
clear(): void {
|
|
134
|
+
this.#cacheKey = "";
|
|
135
|
+
this.#contexts = [];
|
|
136
|
+
this.#clipped = new Set();
|
|
137
|
+
this.#maskOnly = new Set();
|
|
138
|
+
this.#byIndex.clear();
|
|
139
|
+
}
|
|
140
|
+
}
|
package/src/render/clipping.ts
CHANGED
|
@@ -412,12 +412,28 @@ export function fitClippingContexts(
|
|
|
412
412
|
}));
|
|
413
413
|
}
|
|
414
414
|
|
|
415
|
-
/** Flatten layout to `[x, y, w, h]` for GPU uniforms. */
|
|
416
|
-
export function maskLayoutVec4(
|
|
417
|
-
|
|
415
|
+
/** Flatten layout to `[x, y, w, h]` for GPU uniforms. Reuses `into` when provided. */
|
|
416
|
+
export function maskLayoutVec4(
|
|
417
|
+
layout: MaskLayoutRect,
|
|
418
|
+
into?: Float32Array,
|
|
419
|
+
): Float32Array {
|
|
420
|
+
const out = into && into.length >= 4 ? into : new Float32Array(4);
|
|
421
|
+
out[0] = layout.x;
|
|
422
|
+
out[1] = layout.y;
|
|
423
|
+
out[2] = layout.width;
|
|
424
|
+
out[3] = layout.height;
|
|
425
|
+
return out;
|
|
418
426
|
}
|
|
419
427
|
|
|
420
|
-
/** Flatten channel flag to `[r, g, b, a]` for GPU uniforms. */
|
|
421
|
-
export function maskChannelVec4(
|
|
422
|
-
|
|
428
|
+
/** Flatten channel flag to `[r, g, b, a]` for GPU uniforms. Reuses `into` when provided. */
|
|
429
|
+
export function maskChannelVec4(
|
|
430
|
+
flag: MaskChannelFlag,
|
|
431
|
+
into?: Float32Array,
|
|
432
|
+
): Float32Array {
|
|
433
|
+
const out = into && into.length >= 4 ? into : new Float32Array(4);
|
|
434
|
+
out[0] = flag[0];
|
|
435
|
+
out[1] = flag[1];
|
|
436
|
+
out[2] = flag[2];
|
|
437
|
+
out[3] = flag[3];
|
|
438
|
+
return out;
|
|
423
439
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pos/UV interleave with optional resident output buffer (GPU upload scratch).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Interleave `[x,y]` + `[u,v]` into `[x,y,u,v,…]`. Reuses `into` when large enough. */
|
|
6
|
+
export function interleavePosUv(
|
|
7
|
+
positions: Float32Array,
|
|
8
|
+
uvs: Float32Array,
|
|
9
|
+
into?: Float32Array,
|
|
10
|
+
): Float32Array {
|
|
11
|
+
const n = Math.floor(positions.length / 2);
|
|
12
|
+
const need = n * 4;
|
|
13
|
+
const out = into && into.length >= need ? into : new Float32Array(need);
|
|
14
|
+
for (let i = 0; i < n; i++) {
|
|
15
|
+
const o = i * 4;
|
|
16
|
+
const p = i * 2;
|
|
17
|
+
out[o] = positions[p]!;
|
|
18
|
+
out[o + 1] = positions[p + 1]!;
|
|
19
|
+
out[o + 2] = uvs[p] ?? 0;
|
|
20
|
+
out[o + 3] = uvs[p + 1] ?? 0;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Byte length of the live interleaved prefix (vertexCount * 16). */
|
|
26
|
+
export function interleaveByteLength(positions: Float32Array): number {
|
|
27
|
+
return Math.floor(positions.length / 2) * 16;
|
|
28
|
+
}
|
|
@@ -69,6 +69,12 @@ export interface ModelBackend {
|
|
|
69
69
|
/** Optional PartOpacity override (moc3 parts / pose / motion). */
|
|
70
70
|
setPartOpacity?(model: InternalModel, id: string, value: number): void;
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Resolve parameter id → index (O(1) after load).
|
|
74
|
+
* Used by motion blend / hosts instead of `listParameters().find`.
|
|
75
|
+
*/
|
|
76
|
+
resolveParameter?(model: InternalModel, id: string): number | undefined;
|
|
77
|
+
|
|
72
78
|
listParameters?(model: InternalModel): readonly ParameterBinding[];
|
|
73
79
|
}
|
|
74
80
|
|
package/src/types.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface TextureData {
|
|
|
36
36
|
export interface ModelDrawPass {
|
|
37
37
|
setTextures(textures: TextureData[]): void;
|
|
38
38
|
|
|
39
|
-
draw(drawables: DrawableMesh[], modelMatrix: Float32Array): void;
|
|
39
|
+
draw(drawables: readonly DrawableMesh[], modelMatrix: Float32Array): void;
|
|
40
40
|
|
|
41
41
|
destroy(): void;
|
|
42
42
|
}
|