spine-rigc 2.55.0 → 2.57.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 +4 -2
- package/docs/AUTHORING.md +549 -8
- package/package.json +1 -1
- package/skills/rigc/SKILL.md +4 -1
- package/src/anglerange.ts +190 -0
- package/src/cli/core_commands.ts +218 -13
- package/src/cli/shared.ts +160 -9
- package/src/compile.ts +69 -8
- package/src/core/entry.ts +6 -1
- package/src/deformgen.ts +333 -2
- package/src/meshsplit.ts +683 -0
- package/src/motion.ts +20 -8
- package/src/render_shared.ts +30 -0
- package/src/rig.ts +95 -3
- package/src/stop.ts +472 -0
- package/src/transform.ts +20 -0
- package/src/viewportpin.ts +168 -0
- package/src/whirl.ts +580 -0
package/src/deformgen.ts
CHANGED
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
|
|
58
58
|
import { CompileError } from './errors.ts';
|
|
59
59
|
import { hypot2 } from './numeric.ts';
|
|
60
|
+
import { RIG_INFLUENCE_SHAPES } from './rig.ts';
|
|
60
61
|
|
|
61
62
|
/** Which coordinate a bend or a wave reads, and which one it displaces. */
|
|
62
63
|
export type DeformAxis = 'x' | 'y';
|
|
@@ -120,6 +121,8 @@ export interface DeformTurn {
|
|
|
120
121
|
degrees: number;
|
|
121
122
|
/** Where the axis crosses the driving coordinate. Default 0. */
|
|
122
123
|
about?: number;
|
|
124
|
+
/** Scales each vertex's offset by a weight in [0, 1] — see `DeformEnvelope`. */
|
|
125
|
+
envelope?: DeformEnvelope;
|
|
123
126
|
}
|
|
124
127
|
|
|
125
128
|
/**
|
|
@@ -143,6 +146,8 @@ export interface DeformAffine {
|
|
|
143
146
|
scale: [number, number];
|
|
144
147
|
/** The fixed point, in the attachment's own units. Default `[0, 0]`. */
|
|
145
148
|
about?: [number, number];
|
|
149
|
+
/** Scales each vertex's offset by a weight in [0, 1] — see `DeformEnvelope`. */
|
|
150
|
+
envelope?: DeformEnvelope;
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
/**
|
|
@@ -170,6 +175,8 @@ export interface DeformWave {
|
|
|
170
175
|
along: DeformAxis;
|
|
171
176
|
/** The coordinate it displaces. */
|
|
172
177
|
axis: DeformAxis;
|
|
178
|
+
/** Scales each vertex's offset by a weight in [0, 1] — see `DeformEnvelope`. */
|
|
179
|
+
envelope?: DeformEnvelope;
|
|
173
180
|
}
|
|
174
181
|
|
|
175
182
|
/**
|
|
@@ -196,10 +203,96 @@ export interface DeformBend {
|
|
|
196
203
|
along: DeformAxis;
|
|
197
204
|
/** The coordinate it displaces. */
|
|
198
205
|
axis: DeformAxis;
|
|
206
|
+
/** Scales each vertex's offset by a weight in [0, 1] — see `DeformEnvelope`. */
|
|
207
|
+
envelope?: DeformEnvelope;
|
|
199
208
|
}
|
|
200
209
|
|
|
201
210
|
export type DeformTransform = DeformTurn | DeformAffine | DeformWave | DeformBend;
|
|
202
211
|
|
|
212
|
+
/**
|
|
213
|
+
* The two curves an envelope rises along: the names `influence.shape` gives its
|
|
214
|
+
* pull (issue #1415), and the same two curves read from the other end — a pull
|
|
215
|
+
* falls from 1 to 0, a weight rises from 0 to 1, and each is `1 − the other`
|
|
216
|
+
* at the same `u`. One vocabulary, so `"smooth"` means one curve in this tree.
|
|
217
|
+
*/
|
|
218
|
+
export const DEFORM_ENVELOPE_SHAPES = RIG_INFLUENCE_SHAPES;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* **Which vertices a transform moves, and how much** (issue #1413) — a weight
|
|
222
|
+
* in [0, 1] per vertex that each vertex's offset is multiplied by before it is
|
|
223
|
+
* written, so "only the hem ripples" is a model rather than a table.
|
|
224
|
+
*
|
|
225
|
+
* Two forms, and a key states exactly one:
|
|
226
|
+
*
|
|
227
|
+
* - **a ramp** — `axis`, `from`, `to`, `shape`. A vertex's position is read
|
|
228
|
+
* along `axis` (`p = axis · position`), `u = (p − from) / (to − from)` is
|
|
229
|
+
* clamped to [0, 1], and the weight is `u` (`"linear"`) or `u²(3 − 2u)`
|
|
230
|
+
* (`"smooth"`). So the weight is 0 at `from` and everywhere before it, 1 at
|
|
231
|
+
* `to` and everywhere past it.
|
|
232
|
+
* - **a mask** — `mask`, the file name of this attachment's own `soft` mask
|
|
233
|
+
* (AUTHORING §3.4.6). The weight is the level that mask was sampled to at the
|
|
234
|
+
* vertex when the mesh was built, black 0 and white 1 — the same number the
|
|
235
|
+
* soft region's binding carries.
|
|
236
|
+
*
|
|
237
|
+
* ⭐ **The ramp is read in the space the transform is evaluated in**, and that
|
|
238
|
+
* is the only space it could be read in: the weight multiplies the offset the
|
|
239
|
+
* model produced at a vertex, so the position that picks the weight has to be
|
|
240
|
+
* the position the model read. That is the attachment's own space while every
|
|
241
|
+
* vertex is one pair of one bone, and setup WORLD past that (§4.11.1) — so
|
|
242
|
+
* `from` and `to` change units exactly when `radius`, `about` and a bend's
|
|
243
|
+
* `from` do, and the report's `derived` line says which.
|
|
244
|
+
*
|
|
245
|
+
* 🔒 **It is baked, not played.** The weighted offsets are the deform table the
|
|
246
|
+
* file carries, so the runtime is asked to do nothing it did not already do and
|
|
247
|
+
* every poser sees the same numbers. Every number here is a field the spec
|
|
248
|
+
* states; none has a default.
|
|
249
|
+
*/
|
|
250
|
+
export interface DeformEnvelope {
|
|
251
|
+
/**
|
|
252
|
+
* The unit direction a vertex's position is read along — `[0, 1]` reads `y`,
|
|
253
|
+
* `[1, 0]` reads `x`, and a diagonal reads across both. Its length is 1 on
|
|
254
|
+
* the model's own 1e-6 grid, or the key is refused: a longer one would scale
|
|
255
|
+
* the positions `from` and `to` are compared with.
|
|
256
|
+
*/
|
|
257
|
+
axis?: [number, number];
|
|
258
|
+
/** The position along `axis` where the weight is 0 — and before which it stays 0. */
|
|
259
|
+
from?: number;
|
|
260
|
+
/** The position along `axis` where the weight is 1 — and past which it stays 1. Differs from `from`. */
|
|
261
|
+
to?: number;
|
|
262
|
+
/** How the weight rises between them: `"linear"` is `u`, `"smooth"` is `u²(3 − 2u)`, flat at both ends. */
|
|
263
|
+
shape?: (typeof DEFORM_ENVELOPE_SHAPES)[number];
|
|
264
|
+
/** The file name of this attachment's own `soft` mask; the weight is its sampled level. Never beside the ramp's fields. */
|
|
265
|
+
mask?: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* A `soft` mask as the mesh builder sampled it: the file the generator named
|
|
270
|
+
* and its level at each vertex, `0..1`, in the emitted vertex order — what a
|
|
271
|
+
* `mask` envelope reads. Kept beside the attachment rather than in it, as a
|
|
272
|
+
* depth map's `z` is, because the format has nowhere to carry it.
|
|
273
|
+
*/
|
|
274
|
+
export interface DeformSoftMask {
|
|
275
|
+
mask: string;
|
|
276
|
+
levels: readonly number[];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** What an envelope did, for `explain` to print beside the model. */
|
|
280
|
+
export interface DeformEnvelopeReport {
|
|
281
|
+
/** The envelope as the spec states it. */
|
|
282
|
+
stated: string;
|
|
283
|
+
/** How a vertex's weight is computed, written out. */
|
|
284
|
+
formula: string;
|
|
285
|
+
/**
|
|
286
|
+
* Each vertex's weight, on the model's 1e-6 grid — the number its offset was
|
|
287
|
+
* multiplied by. Every emitted offset is `round(weight × the un-enveloped
|
|
288
|
+
* offset)`, so a reader reproduces the run from this and the model.
|
|
289
|
+
*/
|
|
290
|
+
weights: number[];
|
|
291
|
+
/** How many vertices took weight 0, and how many weight 1. */
|
|
292
|
+
zero: number;
|
|
293
|
+
full: number;
|
|
294
|
+
}
|
|
295
|
+
|
|
203
296
|
/** The kinds this module evaluates, in the order the docs list them. */
|
|
204
297
|
export const DEFORM_TRANSFORM_KINDS = ['yaw', 'pitch', 'affine', 'wave', 'bend'] as const;
|
|
205
298
|
|
|
@@ -233,8 +326,13 @@ export interface DeformTransformReport {
|
|
|
233
326
|
* multi-influence one it is one WORLD displacement per vertex, and the array
|
|
234
327
|
* the caller wrote from it is `expanded` on the compile result — the report
|
|
235
328
|
* still quotes what was emitted rather than re-evaluating anything.
|
|
329
|
+
*
|
|
330
|
+
* Under an envelope these are the WEIGHTED displacements — what was emitted
|
|
331
|
+
* — and `envelope` carries the weights.
|
|
236
332
|
*/
|
|
237
333
|
offsets: number[];
|
|
334
|
+
/** Present only on a key that states an `envelope` (issue #1413). */
|
|
335
|
+
envelope?: DeformEnvelopeReport;
|
|
238
336
|
}
|
|
239
337
|
|
|
240
338
|
/** The compiler's `onModelGrid` (1e-6, then float32), passed in so the quantiser stays in one place. */
|
|
@@ -271,6 +369,12 @@ export function evaluateDeformTransform(
|
|
|
271
369
|
* name rather than silently falling back to a cylinder.
|
|
272
370
|
*/
|
|
273
371
|
depth: readonly number[] | null = null,
|
|
372
|
+
/**
|
|
373
|
+
* The attachment's `soft` mask as its builder sampled it, or null when its
|
|
374
|
+
* generator named none — what makes a `mask` envelope that names nothing
|
|
375
|
+
* refusable by name (issue #1413).
|
|
376
|
+
*/
|
|
377
|
+
soft: DeformSoftMask | null = null,
|
|
274
378
|
): DeformTransformReport {
|
|
275
379
|
if (transform === null || typeof transform !== 'object' || Array.isArray(transform)) {
|
|
276
380
|
throw new CompileError(`${where}: "transform" is ${JSON.stringify(transform)}; it is an object with a "kind"`);
|
|
@@ -284,6 +388,10 @@ export function evaluateDeformTransform(
|
|
|
284
388
|
}
|
|
285
389
|
const count = setup.length / 2;
|
|
286
390
|
const offsets = new Array<number>(setup.length);
|
|
391
|
+
// An envelope scales each vertex by its own weight, so a proof that holds of
|
|
392
|
+
// the model — an affine's positive determinant, a power-1 bend's shear —
|
|
393
|
+
// says nothing about the enveloped key, and the derived line must not claim it.
|
|
394
|
+
const envelope = (transform as { envelope?: unknown }).envelope;
|
|
287
395
|
let derived: string[];
|
|
288
396
|
let stated: string;
|
|
289
397
|
let formula: string;
|
|
@@ -440,7 +548,13 @@ export function evaluateDeformTransform(
|
|
|
440
548
|
'the key holds still';
|
|
441
549
|
stated = `scale=[${scale[0]}, ${scale[1]}]${a.about === undefined ? '' : ` about=[${about[0]}, ${about[1]}]`}`;
|
|
442
550
|
formula = 'dx = (sx − 1)·(x − ax), dy = (sy − 1)·(y − ay)';
|
|
443
|
-
derived = [
|
|
551
|
+
derived = [
|
|
552
|
+
`sx − 1 = ${round(scale[0] - 1)}`,
|
|
553
|
+
`sy − 1 = ${round(scale[1] - 1)}`,
|
|
554
|
+
envelope === undefined
|
|
555
|
+
? `det = sx·sy = ${round(det)} > 0, so no triangle can reverse`
|
|
556
|
+
: `det = sx·sy = ${round(det)} > 0 un-enveloped; the envelope weighs each vertex alone, so the keyed map is not affine and A39 measures it`,
|
|
557
|
+
];
|
|
444
558
|
break;
|
|
445
559
|
}
|
|
446
560
|
case 'wave': {
|
|
@@ -527,8 +641,10 @@ export function evaluateDeformTransform(
|
|
|
527
641
|
formula = `d${axis === 0 ? 'x' : 'y'} = amount · u^${power}, u = (${an} − from) / (to − from)`;
|
|
528
642
|
derived = [
|
|
529
643
|
`span = to − from = ${round(span)}`,
|
|
530
|
-
power === 1
|
|
644
|
+
power === 1 && envelope === undefined
|
|
531
645
|
? 'power 1 is an affine shear (det = 1), so no triangle can reverse'
|
|
646
|
+
: power === 1
|
|
647
|
+
? 'power 1 is an affine shear (det = 1) un-enveloped; the envelope weighs each vertex alone, so the keyed map is not affine and A39 measures it'
|
|
532
648
|
: `power ${power} is not affine — the gradient at u is ${power}·amount·u^${power - 1}/span, so it is 0 at "from"`,
|
|
533
649
|
];
|
|
534
650
|
break;
|
|
@@ -567,6 +683,49 @@ export function evaluateDeformTransform(
|
|
|
567
683
|
);
|
|
568
684
|
}
|
|
569
685
|
|
|
686
|
+
// -- the envelope (issue #1413) --------------------------------------------
|
|
687
|
+
//
|
|
688
|
+
// Applied to the run the model emits un-enveloped — the ROUNDED offsets, the
|
|
689
|
+
// ones a key without the envelope writes — and rounded again, so every
|
|
690
|
+
// emitted offset is `round(weight × that offset)` and a reader reproduces it
|
|
691
|
+
// from the report's weights and the model. Applied after the refusal above,
|
|
692
|
+
// so a model the geometry sampled to nothing keeps that refusal's diagnosis,
|
|
693
|
+
// and before the two below, which are the envelope's own.
|
|
694
|
+
let envelopeReport: DeformEnvelopeReport | undefined;
|
|
695
|
+
if (envelope !== undefined) {
|
|
696
|
+
const weighed = evaluateDeformEnvelope(envelope, setup, soft, round, where);
|
|
697
|
+
const plain = offsets.slice();
|
|
698
|
+
for (let v = 0; v < count; v++) {
|
|
699
|
+
offsets[2 * v] = round(weighed.weights[v] * plain[2 * v]);
|
|
700
|
+
offsets[2 * v + 1] = round(weighed.weights[v] * plain[2 * v + 1]);
|
|
701
|
+
}
|
|
702
|
+
envelopeReport = weighed;
|
|
703
|
+
const moving = plain.some((d) => d !== 0);
|
|
704
|
+
// ⭐ The same principle as the refusal above, one step on: every field is
|
|
705
|
+
// legal and the envelope still makes the key mean something other than it
|
|
706
|
+
// says. Weighing every moving vertex 0 states a deformation and emits the
|
|
707
|
+
// identity; leaving every emitted number where it was is a field that
|
|
708
|
+
// changes nothing — a reader's false lead about which model produced the
|
|
709
|
+
// run. A key whose model states the identity is exempt from both: the
|
|
710
|
+
// envelope multiplies the zeros it already said.
|
|
711
|
+
if (count > 0 && !identity && moving && offsets.every((d) => d === 0)) {
|
|
712
|
+
throw new CompileError(
|
|
713
|
+
`${where}: transform ${kind} states ${stated} and moves ${plain.filter((d, i) => i % 2 === 0 && (d !== 0 || plain[i + 1] !== 0)).length} ` +
|
|
714
|
+
`of ${count} vertices, and envelope ${weighed.stated} weighs every one of them 0 — so the key states a ` +
|
|
715
|
+
`deformation and emits the identity. ${weighed.reach}`,
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
if (count > 0 && !identity && moving && offsets.every((d, i) => d === plain[i])) {
|
|
719
|
+
throw new CompileError(
|
|
720
|
+
`${where}: transform ${kind} states envelope ${weighed.stated}, and it changes no emitted number: every vertex ` +
|
|
721
|
+
`the model moves takes weight 1 (${weighed.full} of ${count} vertices at 1, ${weighed.zero} at 0), so the run ` +
|
|
722
|
+
`is the un-enveloped one and the envelope is a field a reader would credit for an output it did not shape. ` +
|
|
723
|
+
`${weighed.reach}`,
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
stated = `${stated} envelope(${weighed.stated})`;
|
|
727
|
+
}
|
|
728
|
+
|
|
570
729
|
let maxOffset = 0;
|
|
571
730
|
let maxOffsetVertex = 0;
|
|
572
731
|
for (let v = 0; v < count; v++) {
|
|
@@ -585,9 +744,181 @@ export function evaluateDeformTransform(
|
|
|
585
744
|
maxOffset: round(maxOffset),
|
|
586
745
|
maxOffsetVertex,
|
|
587
746
|
offsets,
|
|
747
|
+
...(envelopeReport === undefined
|
|
748
|
+
? {}
|
|
749
|
+
: {
|
|
750
|
+
envelope: {
|
|
751
|
+
stated: envelopeReport.stated,
|
|
752
|
+
formula: envelopeReport.formula,
|
|
753
|
+
weights: envelopeReport.weights,
|
|
754
|
+
zero: envelopeReport.zero,
|
|
755
|
+
full: envelopeReport.full,
|
|
756
|
+
},
|
|
757
|
+
}),
|
|
588
758
|
};
|
|
589
759
|
}
|
|
590
760
|
|
|
761
|
+
/** The fields of the two envelope forms, in the order a refusal names them. */
|
|
762
|
+
const ENVELOPE_RAMP_FIELDS = ['axis', 'from', 'to', 'shape'] as const;
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Each vertex's envelope weight (issue #1413), refused by field name wherever
|
|
766
|
+
* the envelope cannot mean what it says.
|
|
767
|
+
*
|
|
768
|
+
* `setup` is the array the model was evaluated over, so a ramp reads the same
|
|
769
|
+
* positions in the same space the model did; `soft` is the attachment's
|
|
770
|
+
* sampled mask, or null. Returns the weights on the model's grid with the
|
|
771
|
+
* audit `explain` prints and the reach the two envelope refusals in
|
|
772
|
+
* `evaluateDeformTransform` quote.
|
|
773
|
+
*/
|
|
774
|
+
export function evaluateDeformEnvelope(
|
|
775
|
+
envelope: unknown,
|
|
776
|
+
setup: readonly number[],
|
|
777
|
+
soft: DeformSoftMask | null,
|
|
778
|
+
round: Rounder,
|
|
779
|
+
where: string,
|
|
780
|
+
): DeformEnvelopeReport & { reach: string } {
|
|
781
|
+
if (envelope === null || typeof envelope !== 'object' || Array.isArray(envelope)) {
|
|
782
|
+
throw new CompileError(
|
|
783
|
+
`${where}: transform field "envelope" is ${JSON.stringify(envelope)}; it is an object — a ramp ` +
|
|
784
|
+
'`{ "axis", "from", "to", "shape" }` or a mask `{ "mask" }`',
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
const e = envelope as DeformEnvelope;
|
|
788
|
+
const count = setup.length / 2;
|
|
789
|
+
const rampStated = ENVELOPE_RAMP_FIELDS.filter((f) => e[f] !== undefined);
|
|
790
|
+
if (e.mask !== undefined && rampStated.length > 0) {
|
|
791
|
+
throw new CompileError(
|
|
792
|
+
`${where}: transform field "envelope" states "mask" ${JSON.stringify(e.mask)} beside the ramp's ` +
|
|
793
|
+
`${rampStated.map((f) => `"${f}"`).join(', ')}. They are two answers to one question — what weight each vertex ` +
|
|
794
|
+
'takes — and a key carrying both leaves a reader unable to say which one the output came from. Drop one.',
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
if (e.mask === undefined && rampStated.length === 0) {
|
|
798
|
+
throw new CompileError(
|
|
799
|
+
`${where}: transform field "envelope" is ${JSON.stringify(envelope)}, which states no weight at all. It is a ramp ` +
|
|
800
|
+
'`{ "axis", "from", "to", "shape" }` or a mask `{ "mask" }`; a key that weighs every vertex 1 carries no envelope.',
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
let weights: number[];
|
|
805
|
+
let stated: string;
|
|
806
|
+
let formula: string;
|
|
807
|
+
let reach: string;
|
|
808
|
+
if (e.mask !== undefined) {
|
|
809
|
+
const mask = e.mask;
|
|
810
|
+
if (soft === null) {
|
|
811
|
+
throw new CompileError(
|
|
812
|
+
`${where}: transform field "envelope.mask" is ${JSON.stringify(mask)}, and this attachment has no "soft" block, ` +
|
|
813
|
+
'so the mask names nothing. An envelope\'s mask is the attachment\'s own soft mask (AUTHORING §3.4.6), the ' +
|
|
814
|
+
'one its generator sampled at every vertex when the mesh was built; give the generator a "soft" block, or ' +
|
|
815
|
+
'state a ramp instead.',
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
if (soft.mask !== mask) {
|
|
819
|
+
throw new CompileError(
|
|
820
|
+
`${where}: transform field "envelope.mask" is ${JSON.stringify(mask)}, and this attachment's soft mask is ` +
|
|
821
|
+
`${JSON.stringify(soft.mask)}, so the mask names nothing it was built with. An envelope reads the mask the ` +
|
|
822
|
+
'generator sampled at every vertex, and only that one.',
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
if (soft.levels.length !== count) {
|
|
826
|
+
// Unreachable while the sampler walks the same vertex list the mesh
|
|
827
|
+
// emitted, as for a depth map; stated because a silent mismatch would
|
|
828
|
+
// weigh one vertex by another's level.
|
|
829
|
+
throw new CompileError(`${where}: the soft mask "${mask}" has ${soft.levels.length} sampled levels for ${count} vertices`);
|
|
830
|
+
}
|
|
831
|
+
weights = soft.levels.map((level) => round(level));
|
|
832
|
+
stated = `mask=${mask}`;
|
|
833
|
+
formula = 'w = the soft mask\'s level at the vertex, black 0 and white 1';
|
|
834
|
+
const carried = weights.filter((w) => w > 0).length;
|
|
835
|
+
reach = `The mask "${mask}" weighs ${carried} of ${count} vertices above 0.`;
|
|
836
|
+
} else {
|
|
837
|
+
const axis = pair(e.axis, 'envelope.axis', where);
|
|
838
|
+
const from = num(e.from, 'envelope.from', where);
|
|
839
|
+
const to = num(e.to, 'envelope.to', where);
|
|
840
|
+
const shape = DEFORM_ENVELOPE_SHAPES.find((name) => name === e.shape);
|
|
841
|
+
if (shape === undefined) {
|
|
842
|
+
throw new CompileError(
|
|
843
|
+
`${where}: transform field "envelope.shape" is ${JSON.stringify(e.shape)}; it is one of ` +
|
|
844
|
+
`${DEFORM_ENVELOPE_SHAPES.map((name) => JSON.stringify(name)).join(', ')} — how the weight rises from 0 at ` +
|
|
845
|
+
'"from" to 1 at "to": "linear" is u, "smooth" is u²(3 − 2u)',
|
|
846
|
+
);
|
|
847
|
+
}
|
|
848
|
+
const length = hypot2(axis[0], axis[1]);
|
|
849
|
+
// The model's own resolution decides "unit", rather than a tolerance chosen
|
|
850
|
+
// here: the diagonal written to seven places, `[0.7071068, 0.7071068]`, is
|
|
851
|
+
// 1.0000000266 long and is the unit direction it says, and a length the
|
|
852
|
+
// 1e-6 grid tells apart from 1 would scale every position the ramp
|
|
853
|
+
// compares with `from` and `to`.
|
|
854
|
+
if (round(length - 1) !== 0) {
|
|
855
|
+
throw new CompileError(
|
|
856
|
+
`${where}: transform field "envelope.axis" is [${axis[0]}, ${axis[1]}], whose length is ${round(length)}; it is a ` +
|
|
857
|
+
'unit direction, length 1 on the model\'s 1e-6 grid. A longer or shorter one would scale every position the ' +
|
|
858
|
+
'ramp compares with "from" and "to" — divide it by its length, and state "from" and "to" along the unit one.',
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
if (from === to) {
|
|
862
|
+
throw new CompileError(
|
|
863
|
+
`${where}: transform fields "envelope.from" and "envelope.to" are both ${from}; they are where the weight is 0 ` +
|
|
864
|
+
'and where it is 1, so they differ — a ramp of no length is a step, and a step is the edge a model exists not to leave.',
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
const span = to - from;
|
|
868
|
+
let lo = Infinity;
|
|
869
|
+
let hi = -Infinity;
|
|
870
|
+
weights = new Array<number>(count);
|
|
871
|
+
for (let v = 0; v < count; v++) {
|
|
872
|
+
const p = axis[0] * setup[2 * v] + axis[1] * setup[2 * v + 1];
|
|
873
|
+
if (p < lo) lo = p;
|
|
874
|
+
if (p > hi) hi = p;
|
|
875
|
+
const u = Math.min(1, Math.max(0, (p - from) / span));
|
|
876
|
+
weights[v] = round(shape === 'linear' ? u : u * u * (3 - 2 * u));
|
|
877
|
+
}
|
|
878
|
+
stated = `axis=[${axis[0]}, ${axis[1]}] from=${from} to=${to} shape=${shape}`;
|
|
879
|
+
formula =
|
|
880
|
+
`w = ${shape === 'linear' ? 'u' : 'u²(3 − 2u)'}, u = clamp((axis · position − from) / (to − from), 0, 1)`;
|
|
881
|
+
reach =
|
|
882
|
+
count === 0
|
|
883
|
+
? 'The attachment has no vertices.'
|
|
884
|
+
: `Along [${axis[0]}, ${axis[1]}] this attachment's vertices sit at ${round(lo)} to ${round(hi)}, and the ramp ` +
|
|
885
|
+
`rises from ${from} (weight 0) to ${to} (weight 1) — move "from" and "to" onto the part.`;
|
|
886
|
+
}
|
|
887
|
+
checkEnvelopeWeights(weights, e.mask === undefined ? 'envelope.shape' : 'envelope.mask', where);
|
|
888
|
+
return {
|
|
889
|
+
stated,
|
|
890
|
+
formula,
|
|
891
|
+
weights,
|
|
892
|
+
zero: weights.filter((w) => w === 0).length,
|
|
893
|
+
full: weights.filter((w) => w === 1).length,
|
|
894
|
+
reach,
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Refuse a weight outside [0, 1], naming the field that produced it, the vertex
|
|
900
|
+
* and the value (issue #1413, Bar 2).
|
|
901
|
+
*
|
|
902
|
+
* ⚠️ Nothing a spec states can reach this, and that is stated rather than left
|
|
903
|
+
* to be inferred: a ramp clamps `u` to [0, 1] and both shapes map [0, 1] onto
|
|
904
|
+
* itself, and a mask's level is a sample of a greyscale sheet. It stands
|
|
905
|
+
* because the multiplication is the whole of the envelope's effect and a
|
|
906
|
+
* weight of 1.2 would push a vertex past the model — the shape of defect a
|
|
907
|
+
* later shape or a later mask reader would bring in silently. Exported so the
|
|
908
|
+
* selftest can hand it the weight no envelope produces and see it refuse.
|
|
909
|
+
*/
|
|
910
|
+
export function checkEnvelopeWeights(weights: readonly number[], field: string, where: string): void {
|
|
911
|
+
for (let v = 0; v < weights.length; v++) {
|
|
912
|
+
const w = weights[v];
|
|
913
|
+
if (!(w >= 0 && w <= 1)) {
|
|
914
|
+
throw new CompileError(
|
|
915
|
+
`${where}: transform field "${field}" weighs vertex ${v} at ${w}; an envelope's weight is in [0, 1] — 0 leaves ` +
|
|
916
|
+
'the vertex where it is and 1 moves it the whole way the model says, and nothing past either end is a weight.',
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
591
922
|
/** One required finite number, refused by field name rather than compiled as a NaN. */
|
|
592
923
|
function num(value: unknown, field: string, where: string): number {
|
|
593
924
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|