@rig-c/core 2.71.0 → 2.73.0
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 +10 -0
- package/docs/AUTHORING.md +172 -11
- package/docs/FACE.md +19 -0
- package/package.json +1 -1
- package/src/alpha_clip.ts +255 -0
- package/src/cli/core_commands.ts +2 -1
- package/src/cli/shared.ts +14 -1
- package/src/compile.ts +364 -27
- package/src/core/clipping.ts +5 -1
- package/src/core/events.ts +17 -5
- package/src/deformgen.ts +2 -2
- package/src/mesh.ts +3 -1
- package/src/meshcompare.ts +146 -22
- package/src/rig.ts +32 -7
- package/src/runtime/index.ts +8 -4
- package/src/runtime/model.ts +37 -20
- package/src/runtime/pose.ts +41 -1
- package/src/types.ts +26 -0
package/src/compile.ts
CHANGED
|
@@ -34,6 +34,8 @@ import {
|
|
|
34
34
|
type DepthTone,
|
|
35
35
|
} from './depth.ts';
|
|
36
36
|
import { CompileError, NotImplementedError } from './errors.ts';
|
|
37
|
+
import { alphaClipOutline, clipCarriers, refuseClipCrossing, refuseClipDegeneracy } from './alpha_clip.ts';
|
|
38
|
+
import { regionCorners } from './core/vertices.ts';
|
|
37
39
|
import { parseJsonWithPosition } from './json-position.ts';
|
|
38
40
|
import { hypot2 } from './numeric.ts';
|
|
39
41
|
// The "did you mean" list on a missing atlas region, from the one implementation
|
|
@@ -60,6 +62,7 @@ import {
|
|
|
60
62
|
type RigBone,
|
|
61
63
|
type RigBoundingBoxAttachment,
|
|
62
64
|
type RigClippingAttachment,
|
|
65
|
+
type RigAlphaClip,
|
|
63
66
|
type RigEvent,
|
|
64
67
|
type RigLinkedMeshAttachment,
|
|
65
68
|
type RigMeshAttachment,
|
|
@@ -2296,7 +2299,7 @@ function manifestShapeVisits(manifest: unknown): ShapeVisit[] {
|
|
|
2296
2299
|
*/
|
|
2297
2300
|
export function compile(opts: CompileOptions): CompileResult {
|
|
2298
2301
|
return withDroppedStates((droppedStates) => {
|
|
2299
|
-
const { model, header, atlasText, declaredDurations } = compileInto(opts, droppedStates);
|
|
2302
|
+
const { model, header, atlasText, declaredDurations, alphaClips } = compileInto(opts, droppedStates);
|
|
2300
2303
|
// The skeleton: the Spine emitter's one entry (issue #922), which writes
|
|
2301
2304
|
// every section from the model, in the order and under the spellings the
|
|
2302
2305
|
// file has, and runs the parser-default and key-order passes on the
|
|
@@ -2314,6 +2317,7 @@ export function compile(opts: CompileOptions): CompileResult {
|
|
|
2314
2317
|
atlasText,
|
|
2315
2318
|
declaredDurations,
|
|
2316
2319
|
...carriedOf(model),
|
|
2320
|
+
...(alphaClips?.length ? { alphaClips } : {}),
|
|
2317
2321
|
model,
|
|
2318
2322
|
};
|
|
2319
2323
|
});
|
|
@@ -2391,6 +2395,7 @@ export function headerBoundsOf(model: CompiledModel, atlasText: string): { box:
|
|
|
2391
2395
|
* beside it to gate it, write its document and — if it asks — emit it.
|
|
2392
2396
|
*/
|
|
2393
2397
|
export interface CompiledBuild {
|
|
2398
|
+
alphaClips?: CompileResult['alphaClips'];
|
|
2394
2399
|
/** The compiled model (`src/model.ts`), `stage` and `editorOrder` included (issue #1026). */
|
|
2395
2400
|
model: CompiledModel;
|
|
2396
2401
|
/** The rest of the Spine header the model does not hold (`SkeletonHeader`): what `compile` hands the emitter beside the model. */
|
|
@@ -3011,7 +3016,8 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3011
3016
|
const meshes: CompileResult['meshes'] = [];
|
|
3012
3017
|
// `skin/slot/attachment` -> per-vertex z, for the attachments that named a
|
|
3013
3018
|
// depth map. Read at deform-key time; never emitted. See `AttachmentContext`.
|
|
3014
|
-
const attachmentDepths = new Map<string, number[]>();
|
|
3019
|
+
const attachmentDepths = new Map<string, number[] | { z: number[]; geometry: DeformGeometry }>();
|
|
3020
|
+
const pendingAlphaClips: PendingAlphaClip[] = [];
|
|
3015
3021
|
// The same for a `soft` mask's per-vertex level, which a deform envelope's
|
|
3016
3022
|
// `mask` reads (issue #1413). Never emitted either.
|
|
3017
3023
|
const attachmentSoftMasks = new Map<string, DeformSoftMask>();
|
|
@@ -3212,6 +3218,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3212
3218
|
const where = `skin "${skinName}" slot "${rigSlot.name}" attachment "${placeholder}"`;
|
|
3213
3219
|
const built = buildRigAttachment(att, placeholder, where, {
|
|
3214
3220
|
images,
|
|
3221
|
+
pendingAlphaClips,
|
|
3215
3222
|
bones,
|
|
3216
3223
|
transforms,
|
|
3217
3224
|
exactFrames,
|
|
@@ -3241,6 +3248,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3241
3248
|
// line a linked mesh is the one construct in a rig spec whose name has not been
|
|
3242
3249
|
// resolved yet.
|
|
3243
3250
|
resolveLinkedMeshes(pendingLinks, skinTables);
|
|
3251
|
+
const alphaClips = resolveAlphaClips(pendingAlphaClips, skinTables, rig, motion);
|
|
3244
3252
|
// 📐 The implicit budget of 0 is a statement about rigc's own GENERATORS:
|
|
3245
3253
|
// geometry rigc built is geometry rigc will not ship unmeasured, and
|
|
3246
3254
|
// `A13_MESH_BUDGET` has nothing to measure a generated mesh against until the
|
|
@@ -3729,6 +3737,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3729
3737
|
if (deformTimelines.get(skinName)?.get(track.slot)?.has(track.attachment)) {
|
|
3730
3738
|
throw new CompileError(`${at}: two deform timelines on one attachment; merge them into one`, 'CE_COMPILE_DEFORM_TIMELINE_DUPLICATE');
|
|
3731
3739
|
}
|
|
3740
|
+
const sampledDepth = attachmentDepths.get(`${skinName}/${track.slot}/${track.attachment}`);
|
|
3732
3741
|
const keys = compileDeformTrack(
|
|
3733
3742
|
track,
|
|
3734
3743
|
motion,
|
|
@@ -3736,10 +3745,11 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3736
3745
|
anim.duration,
|
|
3737
3746
|
{
|
|
3738
3747
|
...deformGeometryOf(attachment, at, transforms),
|
|
3739
|
-
depth:
|
|
3748
|
+
depth: (Array.isArray(sampledDepth) ? sampledDepth : sampledDepth?.z) ?? null,
|
|
3740
3749
|
soft: attachmentSoftMasks.get(`${skinName}/${track.slot}/${track.attachment}`) ?? null,
|
|
3741
3750
|
},
|
|
3742
3751
|
deformTransforms,
|
|
3752
|
+
sampledDepth !== undefined && !Array.isArray(sampledDepth) ? sampledDepth.geometry : null,
|
|
3743
3753
|
);
|
|
3744
3754
|
for (const key of keys) compiledDuration = Math.max(compiledDuration, key.time);
|
|
3745
3755
|
attachmentTimelinesOf(skinName, track.slot).set(track.attachment, { deform: keys });
|
|
@@ -3918,6 +3928,7 @@ function compileInto(opts: CompileOptions, droppedStates: DroppedState[]): Compi
|
|
|
3918
3928
|
header,
|
|
3919
3929
|
atlasText,
|
|
3920
3930
|
declaredDurations,
|
|
3931
|
+
...(alphaClips.length ? { alphaClips } : {}),
|
|
3921
3932
|
};
|
|
3922
3933
|
}
|
|
3923
3934
|
|
|
@@ -4340,6 +4351,7 @@ function refuseDefaultSkinContests(skinNames: readonly string[], skinParts: Map<
|
|
|
4340
4351
|
// ---------------------------------------------------------------------------
|
|
4341
4352
|
|
|
4342
4353
|
interface AttachmentContext {
|
|
4354
|
+
pendingAlphaClips: PendingAlphaClip[];
|
|
4343
4355
|
images: CompiledImage[];
|
|
4344
4356
|
bones: ModelBone[];
|
|
4345
4357
|
transforms: Map<string, BoneTransform>;
|
|
@@ -4367,7 +4379,7 @@ interface AttachmentContext {
|
|
|
4367
4379
|
* round trip and then vanish — and the whole point of this table is to be
|
|
4368
4380
|
* read by `evaluateDeformTransform` at key time and never emitted at all.
|
|
4369
4381
|
*/
|
|
4370
|
-
depths: Map<string, number[]>;
|
|
4382
|
+
depths: Map<string, number[] | { z: number[]; geometry: DeformGeometry }>;
|
|
4371
4383
|
/**
|
|
4372
4384
|
* A `soft` mask's file and its sampled level per vertex, keyed like `depths`
|
|
4373
4385
|
* — what a deform envelope's `mask` reads at key time (issue #1413). Never
|
|
@@ -4931,6 +4943,273 @@ function buildRigBoundingBox(
|
|
|
4931
4943
|
return out;
|
|
4932
4944
|
}
|
|
4933
4945
|
|
|
4946
|
+
interface PendingAlphaClip {
|
|
4947
|
+
spec: RigAlphaClip;
|
|
4948
|
+
out: ModelClippingAttachment;
|
|
4949
|
+
where: string;
|
|
4950
|
+
ctx: AttachmentContext;
|
|
4951
|
+
}
|
|
4952
|
+
|
|
4953
|
+
/** Resolve after all sources/links exist: draw order cannot decide whether a source can be measured. */
|
|
4954
|
+
function resolveAlphaClips(
|
|
4955
|
+
pending: PendingAlphaClip[],
|
|
4956
|
+
tables: Map<string, SkinTable>,
|
|
4957
|
+
rig: RigSpec,
|
|
4958
|
+
motion: MotionSpec,
|
|
4959
|
+
): NonNullable<CompileResult['alphaClips']> {
|
|
4960
|
+
const reports: NonNullable<CompileResult['alphaClips']> = [];
|
|
4961
|
+
for (const { spec, out, where, ctx } of pending) {
|
|
4962
|
+
const targetTable = tables.get(ctx.skinName)?.[ctx.slotName];
|
|
4963
|
+
const target = Object.entries(targetTable ?? {}).find(([, value]) => value === out)?.[0];
|
|
4964
|
+
// withStatedName can return a fresh object when name was declared.
|
|
4965
|
+
const targetKey = target ?? Object.entries(targetTable ?? {}).find(([, value]) => value.kind === 'clipping' && value.vertices === out.vertices)?.[0];
|
|
4966
|
+
if (targetKey === undefined) {
|
|
4967
|
+
throw new CompileError(
|
|
4968
|
+
`${where}: generated clipping target was not filed in its skin; required a resolved attachment.`,
|
|
4969
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
4970
|
+
);
|
|
4971
|
+
}
|
|
4972
|
+
for (const key of ['slot', 'skin', 'attachment'] as const) {
|
|
4973
|
+
if (typeof spec[key] !== 'string' || spec[key].length === 0) {
|
|
4974
|
+
throw new CompileError(
|
|
4975
|
+
`${where}: alphaClip.${key} is ${JSON.stringify(spec[key])}; required an explicit nonempty source name.`,
|
|
4976
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
4977
|
+
);
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
const sourceWhere = `${spec.skin}/${spec.slot}/${spec.attachment}`;
|
|
4981
|
+
const label = `${where} alphaClip from "${sourceWhere}"`;
|
|
4982
|
+
const source = tables.get(spec.skin)?.[spec.slot]?.[spec.attachment];
|
|
4983
|
+
const sourceSlot = rig.slots.find((slot) => slot.name === spec.slot);
|
|
4984
|
+
if (!source || !sourceSlot) {
|
|
4985
|
+
throw new CompileError(
|
|
4986
|
+
`${label}: source attachment is absent; required a declared slot/skin/attachment.`,
|
|
4987
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
4988
|
+
);
|
|
4989
|
+
}
|
|
4990
|
+
if (source.kind !== 'region' && source.kind !== 'mesh' && source.kind !== 'linkedmesh') {
|
|
4991
|
+
throw new CompileError(
|
|
4992
|
+
`${label}: source kind is ${source.kind}; required a region or resolved mesh with pixels.`,
|
|
4993
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
4994
|
+
);
|
|
4995
|
+
}
|
|
4996
|
+
let geometry: SkinTableEntry = source;
|
|
4997
|
+
let depthKey = sourceWhere;
|
|
4998
|
+
if (geometry.kind === 'linkedmesh') {
|
|
4999
|
+
depthKey = `${geometry.skin}/${geometry.slot}/${geometry.source}`;
|
|
5000
|
+
const base = tables.get(geometry.skin)?.[geometry.slot]?.[geometry.source];
|
|
5001
|
+
if (!base || base.kind !== 'mesh') {
|
|
5002
|
+
throw new CompileError(
|
|
5003
|
+
`${label}: linked source resolves to ${base?.kind ?? 'nothing'}; required a resolved mesh.`,
|
|
5004
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
5005
|
+
);
|
|
5006
|
+
}
|
|
5007
|
+
geometry = base;
|
|
5008
|
+
}
|
|
5009
|
+
if (geometry.kind !== 'region' && geometry.kind !== 'mesh') {
|
|
5010
|
+
throw new CompileError(
|
|
5011
|
+
`${label}: source is ${(geometry as SkinTableEntry).kind}; required region or mesh geometry with a plate (a clipping source would be a dependency cycle).`,
|
|
5012
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
5013
|
+
);
|
|
5014
|
+
}
|
|
5015
|
+
if (source.sequence !== undefined) {
|
|
5016
|
+
throw new CompileError(
|
|
5017
|
+
`${label}: source has a ${source.sequence.count}-frame sequence; required one immutable source plate. Select a separate named attachment for the measured frame.`,
|
|
5018
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
5019
|
+
);
|
|
5020
|
+
}
|
|
5021
|
+
const region = source.path ?? source.name ?? spec.attachment;
|
|
5022
|
+
const img = ctx.images.find((image) => image.region === region);
|
|
5023
|
+
if (!img) {
|
|
5024
|
+
throw new CompileError(
|
|
5025
|
+
`${label}: region "${region}" has no source plate; required image/atlas pixels.`,
|
|
5026
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
5027
|
+
);
|
|
5028
|
+
}
|
|
5029
|
+
if (img.pageGrid !== undefined) {
|
|
5030
|
+
throw new CompileError(
|
|
5031
|
+
`${label}: source plate grid cannot be located. ${img.pageGrid.sentence}`,
|
|
5032
|
+
'CE_COMPILE_ALPHA_CLIP_SOURCE',
|
|
5033
|
+
);
|
|
5034
|
+
}
|
|
5035
|
+
const plate = partPlate(img);
|
|
5036
|
+
const mask = { width: plate.width, height: plate.height, alpha: plateAlpha(plate) };
|
|
5037
|
+
const traced = alphaClipOutline(mask, spec, label, img.atlasScale ?? 1);
|
|
5038
|
+
const ring = traced.points.map(([x, y]): [number, number] => [x / plate.width, y / plate.height]);
|
|
5039
|
+
const anchor = ctx.transforms.get(sourceSlot.bone)!;
|
|
5040
|
+
let sourceBindings: ModelBinding[][];
|
|
5041
|
+
let uvs: number[];
|
|
5042
|
+
let triangles: number[];
|
|
5043
|
+
if (geometry.kind === 'region') {
|
|
5044
|
+
// Full restored plate, not trimmed page rectangle: source crop pixels remain the coordinates.
|
|
5045
|
+
const corners = regionCorners({ ...geometry, atlas: { width: img.width, height: img.height, originalWidth: img.width, originalHeight: img.height, offsetX: 0, offsetY: 0 } }, anchor);
|
|
5046
|
+
uvs = [0, 1, 0, 0, 1, 0, 1, 1];
|
|
5047
|
+
triangles = [0, 1, 2, 2, 3, 0];
|
|
5048
|
+
sourceBindings = Array.from({ length: 4 }, (_, i) => {
|
|
5049
|
+
const [x, y] = toBoneLocal(anchor, corners[2 * i], corners[2 * i + 1]);
|
|
5050
|
+
return [{ bone: sourceSlot.bone, x, y, weight: 1 }];
|
|
5051
|
+
});
|
|
5052
|
+
} else {
|
|
5053
|
+
uvs = geometry.uvs;
|
|
5054
|
+
triangles = geometry.triangles;
|
|
5055
|
+
const vertices = geometry.vertices;
|
|
5056
|
+
sourceBindings = vertices.weighted ? vertices.bindings :
|
|
5057
|
+
Array.from({ length: vertices.xy.length / 2 }, (_, i) => [{
|
|
5058
|
+
bone: sourceSlot.bone, x: vertices.xy[2 * i], y: vertices.xy[2 * i + 1], weight: 1,
|
|
5059
|
+
}]);
|
|
5060
|
+
}
|
|
5061
|
+
const carried = clipCarriers(ring, uvs, triangles, label);
|
|
5062
|
+
if (carried.points.length > spec.maxVertices) {
|
|
5063
|
+
throw new CompileError(
|
|
5064
|
+
`${label}: polygonoverdeclaredvertexbudget: ${carried.points.length} vertices after source carrier crossings (${traced.points.length} simplified), required <=${spec.maxVertices}. Raise the declared budget or simplify the source/outline.`,
|
|
5065
|
+
'CE_COMPILE_ALPHA_CLIP_VERTEX_BUDGET',
|
|
5066
|
+
);
|
|
5067
|
+
}
|
|
5068
|
+
// Per-bone first moments preserve Σβ Σw M(bind) for every bone matrix; no solver or new pose model.
|
|
5069
|
+
const bindings: ModelBinding[][] = carried.carriers.map((carrier) => {
|
|
5070
|
+
const moments = new Map<string, { weight: number; x: number; y: number }>();
|
|
5071
|
+
carrier.corners.forEach((index, corner) => {
|
|
5072
|
+
for (const binding of sourceBindings[index]) {
|
|
5073
|
+
const share = carrier.bary[corner] * binding.weight;
|
|
5074
|
+
const row = moments.get(binding.bone) ?? { weight: 0, x: 0, y: 0 };
|
|
5075
|
+
row.weight += share;
|
|
5076
|
+
row.x += share * binding.x;
|
|
5077
|
+
row.y += share * binding.y;
|
|
5078
|
+
moments.set(binding.bone, row);
|
|
5079
|
+
}
|
|
5080
|
+
});
|
|
5081
|
+
const result: ModelBinding[] = [];
|
|
5082
|
+
for (const [bone, row] of moments) {
|
|
5083
|
+
if (row.weight > 0) {
|
|
5084
|
+
result.push({ bone, weight: f32(row.weight), x: f32(row.x / row.weight), y: f32(row.y / row.weight) });
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
return result;
|
|
5088
|
+
});
|
|
5089
|
+
out.vertices = { weighted: true, bindings };
|
|
5090
|
+
out.vertexCount = bindings.length;
|
|
5091
|
+
const mappedWorld = setupWorldVertices(out.vertices, anchor, ctx.transforms, label)
|
|
5092
|
+
.map(([x, y]): [number, number] => [Math.fround(x), Math.fround(y)]);
|
|
5093
|
+
refuseClipDegeneracy(mappedWorld, `${label} final float32 world coordinates`);
|
|
5094
|
+
refuseClipCrossing(mappedWorld, label);
|
|
5095
|
+
const sourceWorld = sourceBindings.map((vertex): [number, number] => {
|
|
5096
|
+
let x = 0;
|
|
5097
|
+
let y = 0;
|
|
5098
|
+
for (const binding of vertex) {
|
|
5099
|
+
const [wx, wy] = toWorld(ctx.transforms.get(binding.bone)!, binding.x, binding.y);
|
|
5100
|
+
x += binding.weight * wx;
|
|
5101
|
+
y += binding.weight * wy;
|
|
5102
|
+
}
|
|
5103
|
+
return [x, y];
|
|
5104
|
+
});
|
|
5105
|
+
let maxBindingDeviationWorld = 0;
|
|
5106
|
+
carried.carriers.forEach((carrier, index) => {
|
|
5107
|
+
let x = 0;
|
|
5108
|
+
let y = 0;
|
|
5109
|
+
carrier.corners.forEach((corner, k) => {
|
|
5110
|
+
x += carrier.bary[k] * sourceWorld[corner][0];
|
|
5111
|
+
y += carrier.bary[k] * sourceWorld[corner][1];
|
|
5112
|
+
});
|
|
5113
|
+
maxBindingDeviationWorld = Math.max(
|
|
5114
|
+
maxBindingDeviationWorld,
|
|
5115
|
+
hypot2(mappedWorld[index][0] - x, mappedWorld[index][1] - y),
|
|
5116
|
+
);
|
|
5117
|
+
});
|
|
5118
|
+
const filed = targetTable![targetKey];
|
|
5119
|
+
if (filed.kind === 'clipping') {
|
|
5120
|
+
filed.vertices = out.vertices;
|
|
5121
|
+
filed.vertexCount = out.vertexCount;
|
|
5122
|
+
}
|
|
5123
|
+
// Authored depth turns use setup-world geometry, even for a rigid source.
|
|
5124
|
+
// Preserve that frame on the clip's own vertex/influence roster, not the source's.
|
|
5125
|
+
const sourceDepth = ctx.depths.get(depthKey);
|
|
5126
|
+
const z = sourceDepth === undefined || Array.isArray(sourceDepth) ? sourceDepth : sourceDepth.z;
|
|
5127
|
+
let clipDepthGeometry: DeformGeometry | undefined;
|
|
5128
|
+
if (z !== undefined) {
|
|
5129
|
+
const depths = carried.carriers.map((carrier) =>
|
|
5130
|
+
carrier.corners.reduce((sum, vertex, i) => sum + carrier.bary[i] * z[vertex], 0));
|
|
5131
|
+
if (sourceDepth !== undefined && !Array.isArray(sourceDepth)) {
|
|
5132
|
+
clipDepthGeometry = {
|
|
5133
|
+
...deformGeometryOf(out, label, ctx.transforms, ctx.transforms.get(ctx.anchorBone)!),
|
|
5134
|
+
depth: depths,
|
|
5135
|
+
};
|
|
5136
|
+
}
|
|
5137
|
+
ctx.depths.set(`${ctx.skinName}/${ctx.slotName}/${targetKey}`,
|
|
5138
|
+
clipDepthGeometry === undefined ? depths : { z: depths, geometry: clipDepthGeometry });
|
|
5139
|
+
}
|
|
5140
|
+
// The helper, not the number of distinct bone names, defines the frame.
|
|
5141
|
+
// Duplicate influences on one bone also take the world-space path.
|
|
5142
|
+
const frameOf = (attachment: ModelVertexAttachment, unweightedBone: string, depthGeometry: DeformGeometry | undefined,
|
|
5143
|
+
key: MotionDeformTrack['keys'][number] | undefined): {
|
|
5144
|
+
name: string; geometry: DeformGeometry;
|
|
5145
|
+
} => {
|
|
5146
|
+
// Match compileDeformTrack's per-key choice, including authored depth:true.
|
|
5147
|
+
const geometry = (key?.transform !== undefined && 'depth' in key.transform && key.transform.depth === true
|
|
5148
|
+
? depthGeometry : undefined) ?? deformGeometryOf(attachment, label, ctx.transforms);
|
|
5149
|
+
const vertices = attachment.vertices;
|
|
5150
|
+
const bone = vertices.weighted ? vertices.bindings[0]?.[0]?.bone : unweightedBone;
|
|
5151
|
+
return { name: geometry.influenceBones === null ? `bone:${bone}` : 'world', geometry };
|
|
5152
|
+
};
|
|
5153
|
+
for (const [animationName, animation] of Object.entries(motion.animations ?? {})) {
|
|
5154
|
+
const sourceIdentity = source.kind === 'linkedmesh' && source.timelines ? depthKey : sourceWhere;
|
|
5155
|
+
const targetIdentity = `${ctx.skinName}/${ctx.slotName}/${targetKey}`;
|
|
5156
|
+
const sourceTrack = animation.deform?.find((track) =>
|
|
5157
|
+
`${track.skin ?? 'default'}/${track.slot}/${track.attachment}` === sourceIdentity);
|
|
5158
|
+
const clipTrack = animation.deform?.find((track) =>
|
|
5159
|
+
`${track.skin ?? 'default'}/${track.slot}/${track.attachment}` === targetIdentity);
|
|
5160
|
+
if (!sourceTrack && !clipTrack) continue;
|
|
5161
|
+
const sourceDepthGeometry = sourceDepth !== undefined && !Array.isArray(sourceDepth) ? sourceDepth.geometry : undefined;
|
|
5162
|
+
const frames = (sourceTrack?.keys.length ? sourceTrack.keys : [undefined]).map((key, index) => ({
|
|
5163
|
+
source: geometry.kind === 'mesh' ? frameOf(geometry, sourceSlot.bone, sourceDepthGeometry, key) : null,
|
|
5164
|
+
clip: frameOf(out, ctx.anchorBone, clipDepthGeometry, clipTrack?.keys[index]),
|
|
5165
|
+
}));
|
|
5166
|
+
const compatible = frames.every(({ source, clip }) => source !== null && source.geometry.setup !== null &&
|
|
5167
|
+
clip.geometry.setup !== null && source.name === clip.name);
|
|
5168
|
+
const { source: sourceFrame, clip: clipFrame } = frames.find(({ source, clip }) =>
|
|
5169
|
+
source === null || source.geometry.setup === null || clip.geometry.setup === null || source.name !== clip.name) ?? frames[0];
|
|
5170
|
+
// With a world-space deform, varying per-vertex influence shares would
|
|
5171
|
+
// correlate the deform with moving bone matrices. Constant shares commute
|
|
5172
|
+
// with the affine/depth projection; no new deformation transfer is implied.
|
|
5173
|
+
const sharesOf = (vertex: ModelBinding[]): string => {
|
|
5174
|
+
const shares = new Map<string, number>();
|
|
5175
|
+
for (const binding of vertex) {
|
|
5176
|
+
shares.set(binding.bone, (shares.get(binding.bone) ?? 0) + binding.weight);
|
|
5177
|
+
}
|
|
5178
|
+
return JSON.stringify([...shares].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
5179
|
+
};
|
|
5180
|
+
const constantWorldShares = !frames.some(({ source }) => source?.name === 'world') ||
|
|
5181
|
+
sourceBindings.every((vertex) => sharesOf(vertex) === sharesOf(sourceBindings[0]));
|
|
5182
|
+
const shared = sourceTrack !== undefined && clipTrack !== undefined &&
|
|
5183
|
+
JSON.stringify(sourceTrack.keys) === JSON.stringify(clipTrack.keys);
|
|
5184
|
+
const linear = sourceTrack?.keys.every((key) => {
|
|
5185
|
+
const transform = key.transform;
|
|
5186
|
+
return transform !== undefined && transform.envelope === undefined &&
|
|
5187
|
+
(transform.kind === 'affine' || ((transform.kind === 'yaw' || transform.kind === 'pitch') &&
|
|
5188
|
+
transform.depth === true && z !== undefined));
|
|
5189
|
+
});
|
|
5190
|
+
if (!shared || !linear || !compatible || !constantWorldShares) {
|
|
5191
|
+
throw new CompileError(
|
|
5192
|
+
`${label}: animation "${animationName}" has ${sourceTrack?.keys.length ?? 0} source and ` +
|
|
5193
|
+
`${clipTrack?.keys.length ?? 0} clip keys; source frame=${sourceFrame?.name ?? 'no vertex geometry'}, ` +
|
|
5194
|
+
`clip frame=${clipFrame.name}, constant world influence shares=${constantWorldShares}. ` +
|
|
5195
|
+
'Required matching explicit affine/depth-yaw/pitch transforms in compatible DeformGeometry frames, ' +
|
|
5196
|
+
'with constant shares for world-space transfer. Fix the named frame/keys or author corresponding clip motion.',
|
|
5197
|
+
'CE_COMPILE_ALPHA_CLIP_DEFORM',
|
|
5198
|
+
);
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
5201
|
+
reports.push({
|
|
5202
|
+
target: `${ctx.skinName}/${ctx.slotName}/${targetKey}`,
|
|
5203
|
+
source: sourceWhere,
|
|
5204
|
+
...traced.report,
|
|
5205
|
+
vertices: out.vertexCount,
|
|
5206
|
+
uvs: carried.points,
|
|
5207
|
+
maxBindingDeviationWorld,
|
|
5208
|
+
});
|
|
5209
|
+
}
|
|
5210
|
+
return reports;
|
|
5211
|
+
}
|
|
5212
|
+
|
|
4934
5213
|
function buildRigClipping(
|
|
4935
5214
|
att: RigClippingAttachment,
|
|
4936
5215
|
where: string,
|
|
@@ -4954,9 +5233,15 @@ function buildRigClipping(
|
|
|
4954
5233
|
...(att.end !== undefined ? { end: att.end } : {}),
|
|
4955
5234
|
...(att.convex !== undefined ? { convex: att.convex } : {}),
|
|
4956
5235
|
...(att.inverse !== undefined ? { inverse: att.inverse } : {}),
|
|
4957
|
-
vertexCount: att.vertexCount,
|
|
4958
|
-
vertices: buildVertexGeometry(att, where, ctx),
|
|
5236
|
+
vertexCount: att.vertexCount ?? 0,
|
|
5237
|
+
vertices: att.alphaClip === undefined ? buildVertexGeometry({ ...att, vertexCount: att.vertexCount! }, where, ctx) : { weighted: false, xy: [] },
|
|
4959
5238
|
};
|
|
5239
|
+
if (att.alphaClip !== undefined) {
|
|
5240
|
+
const fields = ['vertexCount', 'vertices', 'weights', 'boneIndexing'] as const;
|
|
5241
|
+
const present = fields.filter((key) => att[key] !== undefined);
|
|
5242
|
+
if (present.length || att.convex === true || att.inverse === true) throw new CompileError(`${where}: alphaClip conflicts with [${present.join(', ')}], convex=${att.convex}, inverse=${att.inverse}; required ordinary inside clipping with generated geometry only. Remove authored geometry/flags.`, 'CE_COMPILE_ALPHA_CLIP_CONFLICT');
|
|
5243
|
+
ctx.pendingAlphaClips.push({ spec: att.alphaClip, out, where, ctx });
|
|
5244
|
+
}
|
|
4960
5245
|
if (att.color !== undefined) out.editorColor = att.color;
|
|
4961
5246
|
return out;
|
|
4962
5247
|
}
|
|
@@ -5755,7 +6040,12 @@ function buildRigMesh(
|
|
|
5755
6040
|
if (authored && att.generator) {
|
|
5756
6041
|
throw new CompileError(`${where}: a mesh is either authored geometry or a generator, never both`, 'CE_COMPILE_MESH_AUTHORED_AND_GENERATED');
|
|
5757
6042
|
}
|
|
5758
|
-
if (att.generator)
|
|
6043
|
+
if (att.generator) {
|
|
6044
|
+
if (att.depth !== undefined) {
|
|
6045
|
+
throw new CompileError(`${where}: attachment "depth" describes authored geometry; a generated mesh states its depth on the contour or grid generator instead`, 'CE_COMPILE_DEPTH_AUTHORED_REQUIRED');
|
|
6046
|
+
}
|
|
6047
|
+
return buildGeneratedMesh(att, att.generator, placeholder, where, ctx);
|
|
6048
|
+
}
|
|
5759
6049
|
if (att.vertices && att.weights) {
|
|
5760
6050
|
throw new CompileError(
|
|
5761
6051
|
`${where}: a mesh gives geometry as "vertices" or as "weights", never both — "weights" is the by-name form of the same data`, 'CE_COMPILE_MESH_GEOMETRY_SOURCES_CONFLICT',
|
|
@@ -5833,6 +6123,35 @@ function buildRigMesh(
|
|
|
5833
6123
|
height: f32(height),
|
|
5834
6124
|
};
|
|
5835
6125
|
if (att.sequence !== undefined) out.sequence = buildSequence(att.sequence);
|
|
6126
|
+
let depth: ReturnType<typeof sampleMeshDepth> | undefined;
|
|
6127
|
+
if (att.depth !== undefined) {
|
|
6128
|
+
if (img === undefined) {
|
|
6129
|
+
throw new CompileError(`${where}: an authored mesh with "depth" needs an "image" to locate the part's pixel grid`, 'CE_COMPILE_DEPTH_IMAGE_REQUIRED');
|
|
6130
|
+
}
|
|
6131
|
+
const anchor = ctx.transforms.get(ctx.anchorBone);
|
|
6132
|
+
if (!anchor) throw new CompileError(`${where}: slot bone "${ctx.anchorBone}" has no setup transform`, 'CE_COMPILE_SLOT_SETUP_TRANSFORM_MISSING');
|
|
6133
|
+
const geometry = deformGeometryOf(out, where, ctx.transforms, anchor);
|
|
6134
|
+
if (geometry.setup === null) {
|
|
6135
|
+
throw new CompileError(`${where}: depth needs resolved setup positions — ${geometry.setupWhy}`, 'CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED');
|
|
6136
|
+
}
|
|
6137
|
+
const world = geometry.setup;
|
|
6138
|
+
const plate = partPlate(img);
|
|
6139
|
+
const toArt = img.width / plate.width;
|
|
6140
|
+
// The generator's window is centred on the slot bone's setup WORLD origin.
|
|
6141
|
+
// UVs describe texture placement, not a vertex's position in that window.
|
|
6142
|
+
const points: Array<[number, number]> = [];
|
|
6143
|
+
for (let v = 0; v < world.length; v += 2) {
|
|
6144
|
+
points.push([(world[v] - anchor.worldX + img.width / 2) / toArt,
|
|
6145
|
+
(img.height / 2 - (world[v + 1] - anchor.worldY)) / toArt]);
|
|
6146
|
+
}
|
|
6147
|
+
depth = sampleMeshDepth(att.depth, 'authored', points, att.triangles,
|
|
6148
|
+
(px, py) => [anchor.worldX + px * toArt - img.width / 2, anchor.worldY + img.height / 2 - py * toArt],
|
|
6149
|
+
img.pageGrid === undefined ? plateAlpha(plate) : { unlocated: img.pageGrid.said },
|
|
6150
|
+
sheetGridOf(img, plate), where, ctx,
|
|
6151
|
+
Array.from({ length: world.length / 2 }, (_, v) => [world[2 * v], world[2 * v + 1]]));
|
|
6152
|
+
geometry.depth = depth.z;
|
|
6153
|
+
ctx.depths.set(`${ctx.skinName}/${ctx.slotName}/${placeholder}`, { z: depth.z, geometry });
|
|
6154
|
+
}
|
|
5836
6155
|
// Register it as `authored`: geometry rigc did not build and whose topology it
|
|
5837
6156
|
// therefore gets to assume nothing about. The generator-topology assertions
|
|
5838
6157
|
// read this and skip rather than measuring a ring that was never a ring.
|
|
@@ -5843,6 +6162,7 @@ function buildRigMesh(
|
|
|
5843
6162
|
ctx.meshes.push({
|
|
5844
6163
|
slot: ctx.slotName,
|
|
5845
6164
|
kind: 'authored',
|
|
6165
|
+
...(depth === undefined ? {} : { depth: depth.summary }),
|
|
5846
6166
|
attachments: [ctx.slotName],
|
|
5847
6167
|
vertices: uvCount / 2,
|
|
5848
6168
|
triangles: att.triangles.length / 3,
|
|
@@ -6204,8 +6524,8 @@ function buildGeneratedMesh(
|
|
|
6204
6524
|
* not invent tolerances.
|
|
6205
6525
|
*/
|
|
6206
6526
|
function sampleMeshDepth(
|
|
6207
|
-
spec:
|
|
6208
|
-
kind: 'contour' | 'grid',
|
|
6527
|
+
spec: RigDepthMap,
|
|
6528
|
+
kind: 'contour' | 'grid' | 'authored',
|
|
6209
6529
|
points: ReadonlyArray<readonly [number, number]>,
|
|
6210
6530
|
/** The mesh's own triangle list, for the turn ceiling. */
|
|
6211
6531
|
triangles: ReadonlyArray<number>,
|
|
@@ -6244,6 +6564,8 @@ function sampleMeshDepth(
|
|
|
6244
6564
|
grid: SheetGrid,
|
|
6245
6565
|
where: string,
|
|
6246
6566
|
ctx: AttachmentContext,
|
|
6567
|
+
/** Authored setup-world vertices, already in the depth turn's evaluation space. */
|
|
6568
|
+
turnPoints?: ReadonlyArray<readonly [number, number]>,
|
|
6247
6569
|
): {
|
|
6248
6570
|
z: number[];
|
|
6249
6571
|
/** The tone-curved 0..1 nearness per vertex, which `bind` thresholds. */
|
|
@@ -6327,7 +6649,9 @@ function sampleMeshDepth(
|
|
|
6327
6649
|
kind === 'contour'
|
|
6328
6650
|
? 'A contour mesh puts every vertex ON the silhouette and pushes it out by the margin, so a sheet cut to ' +
|
|
6329
6651
|
'the art stops short of every one of them. Dilate the sheet past the mesh margin, or lower the margin.'
|
|
6330
|
-
:
|
|
6652
|
+
: kind === 'authored'
|
|
6653
|
+
? 'The authored setup vertices reach outside the sheet coverage. Extend the sheet to cover their bilinear taps, or correct the setup geometry.'
|
|
6654
|
+
: 'A grid spans the whole part window, corners included, while a sheet is usually cut to the art — so the ' +
|
|
6331
6655
|
'lattice reaches past it wherever the art does not fill the window. Dilate the sheet to the window, or ' +
|
|
6332
6656
|
'state "us"/"vs" that keep the lattice inside the art.';
|
|
6333
6657
|
throw new CompileError(
|
|
@@ -6368,7 +6692,7 @@ function sampleMeshDepth(
|
|
|
6368
6692
|
range: [f32(lo), f32(hi)],
|
|
6369
6693
|
...(partAlpha instanceof Uint8Array ? { undrawn } : { undrawn: null, unlocated: partAlpha.unlocated }),
|
|
6370
6694
|
ceiling: turnCeiling(
|
|
6371
|
-
points.map(([px, py]) => toBind(px, py)),
|
|
6695
|
+
turnPoints ?? points.map(([px, py]) => toBind(px, py)),
|
|
6372
6696
|
z,
|
|
6373
6697
|
triangles,
|
|
6374
6698
|
ctx.turnNarrowing === undefined ? undefined : { narrowing: ctx.turnNarrowing },
|
|
@@ -8737,7 +9061,7 @@ export interface DeformGeometry {
|
|
|
8737
9061
|
/** Why `setup` is null, phrased for the refusal. Null when it is not. */
|
|
8738
9062
|
setupWhy: string | null;
|
|
8739
9063
|
/**
|
|
8740
|
-
* Per-vertex `z`, when this attachment
|
|
9064
|
+
* Per-vertex `z`, when this attachment or its generator named a depth map.
|
|
8741
9065
|
*
|
|
8742
9066
|
* Not measured from the attachment like everything else here — there
|
|
8743
9067
|
* is nowhere in the format to measure it FROM, which is the whole reason the
|
|
@@ -8771,6 +9095,8 @@ export function deformGeometryOf(
|
|
|
8771
9095
|
where: string,
|
|
8772
9096
|
/** Every bone's setup world transform, by name. */
|
|
8773
9097
|
transforms: Map<string, BoneTransform>,
|
|
9098
|
+
/** Force the world-offset path for an authored depth turn, including rigid meshes. */
|
|
9099
|
+
depthAnchor?: BoneTransform,
|
|
8774
9100
|
): DeformGeometry {
|
|
8775
9101
|
const type = att.kind;
|
|
8776
9102
|
let worldVerticesLength: number;
|
|
@@ -8816,6 +9142,15 @@ export function deformGeometryOf(
|
|
|
8816
9142
|
const vertices = att.vertices;
|
|
8817
9143
|
const weighted = vertices.weighted;
|
|
8818
9144
|
if (!vertices.weighted) {
|
|
9145
|
+
if (depthAnchor !== undefined) {
|
|
9146
|
+
return {
|
|
9147
|
+
weighted, deformLength: worldVerticesLength, vertexCount: worldVerticesLength / 2,
|
|
9148
|
+
boneCounts: Array(worldVerticesLength / 2).fill(1),
|
|
9149
|
+
setup: setupWorldVertices(vertices, depthAnchor, transforms, where).flat(),
|
|
9150
|
+
influenceBones: Array(worldVerticesLength / 2).fill(depthAnchor),
|
|
9151
|
+
setupWhy: null, depth: null, soft: null,
|
|
9152
|
+
};
|
|
9153
|
+
}
|
|
8819
9154
|
return {
|
|
8820
9155
|
weighted,
|
|
8821
9156
|
deformLength: worldVerticesLength,
|
|
@@ -8879,7 +9214,7 @@ export function deformGeometryOf(
|
|
|
8879
9214
|
// the runtime's last bits (`b = −2.3e-8` on an unrotated bone) into its
|
|
8880
9215
|
// offset, about 1e-6 on gallery/look's hair locks. Evaluating on the authored
|
|
8881
9216
|
// positions instead would need them carried here from the binder.
|
|
8882
|
-
if (boneCounts.every((n) => n === 1) && bones.size === 1) {
|
|
9217
|
+
if (depthAnchor === undefined && boneCounts.every((n) => n === 1) && bones.size === 1) {
|
|
8883
9218
|
return { ...common, setup: bindSpace, influenceBones: null, setupWhy: null };
|
|
8884
9219
|
}
|
|
8885
9220
|
// -- several influences, evaluated in world (issue #389) -------------------
|
|
@@ -9055,6 +9390,7 @@ function compileDeformTrack(
|
|
|
9055
9390
|
duration: number,
|
|
9056
9391
|
geometry: DeformGeometry,
|
|
9057
9392
|
generated: CompileResult['deformTransforms'],
|
|
9393
|
+
depthWorld: DeformGeometry | null = null,
|
|
9058
9394
|
): ModelKey[] {
|
|
9059
9395
|
const skin = track.skin ?? 'default';
|
|
9060
9396
|
const where = `animation "${animName}" deform ${skin}/${track.slot}/${track.attachment}`;
|
|
@@ -9080,6 +9416,7 @@ function compileDeformTrack(
|
|
|
9080
9416
|
// definite, which the arithmetic does for any weighting that closes at 1 and
|
|
9081
9417
|
// for no weighting that does not (issue #389).
|
|
9082
9418
|
if (key.transform !== undefined) {
|
|
9419
|
+
const evaluated = ('depth' in key.transform && key.transform.depth === true ? depthWorld : null) ?? geometry;
|
|
9083
9420
|
if (key.vertices !== undefined && key.vertices !== null) {
|
|
9084
9421
|
throw new CompileError(
|
|
9085
9422
|
`${where} (t=${key.t}): the key carries both a "transform" and a "vertices" run, and they are two answers to ` +
|
|
@@ -9090,33 +9427,33 @@ function compileDeformTrack(
|
|
|
9090
9427
|
if (key.offset !== undefined || key.fromVertex !== undefined) {
|
|
9091
9428
|
throw new CompileError(
|
|
9092
9429
|
`${where} (t=${key.t}): the key states a "transform" and a start index. A transform is a model of the whole ` +
|
|
9093
|
-
`attachment and is evaluated over all ${
|
|
9430
|
+
`attachment and is evaluated over all ${evaluated.vertexCount} of its vertices, so it always starts at deform ` +
|
|
9094
9431
|
'index 0. A model applied to part of a run leaves a step at the run\'s edge — write the partial run by hand ' +
|
|
9095
9432
|
'if that is what you mean.', 'CE_COMPILE_DEFORM_TRANSFORM_START_INDEX',
|
|
9096
9433
|
);
|
|
9097
9434
|
}
|
|
9098
|
-
if (
|
|
9435
|
+
if (evaluated.setup === null) {
|
|
9099
9436
|
throw new CompileError(
|
|
9100
9437
|
`${where} (t=${key.t}): a "transform" states where every vertex of this attachment goes, and this one has a ` +
|
|
9101
|
-
`vertex the arithmetic cannot place — ${
|
|
9438
|
+
`vertex the arithmetic cannot place — ${evaluated.setupWhy}. Fix the weights, or write the bind-space pairs ` +
|
|
9102
9439
|
'yourself and start the run with "offset".', 'CE_COMPILE_DEFORM_TRANSFORM_SETUP_UNRESOLVED',
|
|
9103
9440
|
);
|
|
9104
9441
|
}
|
|
9105
9442
|
const report = evaluateDeformTransform(
|
|
9106
9443
|
key.transform,
|
|
9107
|
-
|
|
9444
|
+
evaluated.setup,
|
|
9108
9445
|
onModelGrid,
|
|
9109
9446
|
`${where} (t=${key.t})`,
|
|
9110
|
-
|
|
9111
|
-
|
|
9447
|
+
evaluated.depth,
|
|
9448
|
+
evaluated.soft,
|
|
9112
9449
|
);
|
|
9113
|
-
if (report.offsets.length !==
|
|
9450
|
+
if (report.offsets.length !== evaluated.vertexCount * 2) {
|
|
9114
9451
|
// Unreachable while `setup` is one pair per vertex, which is the whole
|
|
9115
9452
|
// reason both are derived from the same walk. Stated rather than
|
|
9116
9453
|
// assumed: a silent mismatch here is the overrun A35 exists for.
|
|
9117
9454
|
throw new CompileError(
|
|
9118
9455
|
`${where} (t=${key.t}): the transform produced ${report.offsets.length} numbers for ` +
|
|
9119
|
-
`${
|
|
9456
|
+
`${evaluated.vertexCount} vertices`, 'CE_COMPILE_DEFORM_TRANSFORM_OFFSET_COUNT',
|
|
9120
9457
|
);
|
|
9121
9458
|
}
|
|
9122
9459
|
// -- one displacement per vertex -> one pair per influence (issue #389) -
|
|
@@ -9128,11 +9465,11 @@ function compileDeformTrack(
|
|
|
9128
9465
|
// displacement it expands is the ROUNDED one the report prints, so a
|
|
9129
9466
|
// reader can reproduce every emitted pair from the audit rather than from
|
|
9130
9467
|
// a second evaluation.
|
|
9131
|
-
const run = expandDeformToInfluences(report.offsets,
|
|
9132
|
-
if (run.length !==
|
|
9468
|
+
const run = expandDeformToInfluences(report.offsets, evaluated);
|
|
9469
|
+
if (run.length !== evaluated.deformLength) {
|
|
9133
9470
|
throw new CompileError(
|
|
9134
9471
|
`${where} (t=${key.t}): the transform expanded to ${run.length} numbers and this attachment's deform ` +
|
|
9135
|
-
`array is ${
|
|
9472
|
+
`array is ${evaluated.deformLength} long`, 'CE_COMPILE_DEFORM_TRANSFORM_RUN_LENGTH',
|
|
9136
9473
|
);
|
|
9137
9474
|
}
|
|
9138
9475
|
generated.push({
|
|
@@ -9148,14 +9485,14 @@ function compileDeformTrack(
|
|
|
9148
9485
|
// other way to know they were not evaluated alike. Said only on the path
|
|
9149
9486
|
// where it is not the attachment's own space, so every existing report
|
|
9150
9487
|
// prints exactly the lines it printed before.
|
|
9151
|
-
...(
|
|
9488
|
+
...(evaluated.influenceBones === null
|
|
9152
9489
|
? {}
|
|
9153
9490
|
: {
|
|
9154
9491
|
expanded: run,
|
|
9155
9492
|
derived: [
|
|
9156
9493
|
...report.derived,
|
|
9157
|
-
`evaluated at setup WORLD positions, because ${
|
|
9158
|
-
`${
|
|
9494
|
+
`evaluated at setup WORLD positions, because ${evaluated.vertexCount} vertices carry ` +
|
|
9495
|
+
`${evaluated.deformLength / 2} influences; each displacement D is written into every influence as ` +
|
|
9159
9496
|
'Mᵢ⁻¹·D, so the runtime composes Σwᵢ·Mᵢ·Mᵢ⁻¹·D = D',
|
|
9160
9497
|
],
|
|
9161
9498
|
}),
|
package/src/core/clipping.ts
CHANGED
|
@@ -633,7 +633,11 @@ export function clipThrough(
|
|
|
633
633
|
clip: TriangleClipper = clipTriangles,
|
|
634
634
|
reading: { dropRegion?: number } = {},
|
|
635
635
|
): ClipResult | null {
|
|
636
|
-
|
|
636
|
+
let plan = clipShapeOf(shape);
|
|
637
|
+
// CO11/CL01/CN06 and portal's convex rows need the original world doubles.
|
|
638
|
+
// Two 66-point region/authored up_minus15 pieces witnesses went from 2 to 0
|
|
639
|
+
// rendered pixel differences with Float32 input; SDK clipper storage was not observed.
|
|
640
|
+
if (plan.kind === 'pieces') plan = clipShapeOf({ ...shape, polygon: shape.polygon.map(Math.fround) });
|
|
637
641
|
if (plan.kind === 'refused') return null;
|
|
638
642
|
if (plan.kind === 'convex') return clip(plan.polygon, vertices, triangles, uvs);
|
|
639
643
|
const f = Math.fround;
|