@energy8platform/golem 0.1.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 +376 -0
- package/bin/golem.js +2 -0
- package/dist/editor.css +701 -0
- package/dist/editor.js +59202 -0
- package/dist/lib/cli.d.ts +1 -0
- package/dist/lib/cli.js +5521 -0
- package/dist/lib/cli.js.map +7 -0
- package/dist/lib/editor/api.d.ts +42 -0
- package/dist/lib/editor/app.d.ts +13 -0
- package/dist/lib/editor/atlas-detect.d.ts +28 -0
- package/dist/lib/editor/atlas-panel.d.ts +1 -0
- package/dist/lib/editor/atlas.d.ts +39 -0
- package/dist/lib/editor/gizmo-math.d.ts +36 -0
- package/dist/lib/editor/gizmos.d.ts +185 -0
- package/dist/lib/editor/layers.d.ts +1 -0
- package/dist/lib/editor/library.d.ts +3 -0
- package/dist/lib/editor/panels.d.ts +26 -0
- package/dist/lib/editor/props.d.ts +19 -0
- package/dist/lib/editor/server.d.ts +11 -0
- package/dist/lib/editor/stage.d.ts +222 -0
- package/dist/lib/editor/store.d.ts +1053 -0
- package/dist/lib/editor/timeline-layout.d.ts +64 -0
- package/dist/lib/editor/timeline.d.ts +45 -0
- package/dist/lib/editor-entry.d.ts +2 -0
- package/dist/lib/editor-entry.js +5490 -0
- package/dist/lib/editor-entry.js.map +7 -0
- package/dist/lib/harness.js +51012 -0
- package/dist/lib/ktx2.d.ts +15 -0
- package/dist/lib/mcp-server.d.ts +2 -0
- package/dist/lib/preview/harness.d.ts +16 -0
- package/dist/lib/preview/render-preview.d.ts +27 -0
- package/dist/lib/preview/viewer.d.ts +1 -0
- package/dist/lib/rig-anim.d.ts +109 -0
- package/dist/lib/rig-api.d.ts +26 -0
- package/dist/lib/rig-atlas.d.ts +20 -0
- package/dist/lib/rig-check.d.ts +45 -0
- package/dist/lib/rig-constraints.d.ts +59 -0
- package/dist/lib/rig-deform.d.ts +27 -0
- package/dist/lib/rig-format.d.ts +5036 -0
- package/dist/lib/rig-history.d.ts +21 -0
- package/dist/lib/rig-import-layers.d.ts +32 -0
- package/dist/lib/rig-io.d.ts +9 -0
- package/dist/lib/rig-mesh-image.d.ts +2 -0
- package/dist/lib/rig-mesh.d.ts +143 -0
- package/dist/lib/rig-path.d.ts +101 -0
- package/dist/lib/rig-presets.d.ts +101 -0
- package/dist/lib/rig-queue.d.ts +2 -0
- package/dist/lib/rig-runtime.d.ts +70 -0
- package/dist/lib/rig-state.d.ts +64 -0
- package/dist/lib/rig-symbol.d.ts +60 -0
- package/dist/lib/rig-template-library.d.ts +3 -0
- package/dist/lib/rig-templates.d.ts +46 -0
- package/dist/lib/rig-tools.d.ts +376 -0
- package/dist/lib/runtime.d.ts +9 -0
- package/dist/lib/runtime.js +1584 -0
- package/dist/lib/runtime.js.map +7 -0
- package/dist/lib/spine-import.d.ts +68 -0
- package/dist/lib/tools.d.ts +2 -0
- package/dist/lib/tools.js +5242 -0
- package/dist/lib/tools.js.map +7 -0
- package/editor.html +3 -0
- package/package.json +96 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RigDocument } from "./rig-format";
|
|
2
|
+
export declare function historyDir(rigPath: string): string;
|
|
3
|
+
/** oldest → newest absolute paths */
|
|
4
|
+
export declare function listHistory(rigPath: string): Promise<string[]>;
|
|
5
|
+
export declare const redoDir: (rigPath: string) => string;
|
|
6
|
+
/** oldest → newest absolute paths of redo entries */
|
|
7
|
+
export declare function listRedo(rigPath: string): Promise<string[]>;
|
|
8
|
+
/** Discard every redo entry (invoked whenever a new mutation is pushed). */
|
|
9
|
+
export declare function clearRedo(rigPath: string): Promise<void>;
|
|
10
|
+
/** Store `contents` as the newest snapshot; keeps the last `keep`. A new mutation invalidates redo unless `keepRedo`. */
|
|
11
|
+
export declare function pushHistory(rigPath: string, contents: string, keep?: number, opts?: {
|
|
12
|
+
keepRedo?: boolean;
|
|
13
|
+
}): Promise<string>;
|
|
14
|
+
/** Contents of the `steps`-th newest snapshot; it and everything newer are discarded. */
|
|
15
|
+
export declare function popHistory(rigPath: string, steps?: number): Promise<string>;
|
|
16
|
+
/** Undo `steps` mutations: the current file goes to the redo stack; returns the contents to write back. */
|
|
17
|
+
export declare function undo(rigPath: string, steps?: number): Promise<string>;
|
|
18
|
+
/** Redo the last undo: returns the contents to write back; the pre-redo state is snapshotted into history. */
|
|
19
|
+
export declare function redo(rigPath: string): Promise<string>;
|
|
20
|
+
/** Human-readable structural difference; empty when the documents are identical. */
|
|
21
|
+
export declare function diffDocs(a: RigDocument, b: RigDocument): string[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type RigDocument } from "./rig-format";
|
|
2
|
+
type Doc = RigDocument;
|
|
3
|
+
/**
|
|
4
|
+
* Optional sidecar `layers.json` in the layers dir (written by the segmentation step):
|
|
5
|
+
* [{ "file": "arm_l.png", "id": "arm_l", "role": "arm_l", "parent": "torso", "z": 3,
|
|
6
|
+
* "bbox": [x, y, w, h], "joint": [x, y], "inpainted": true, "prompt": "..." }]
|
|
7
|
+
* Without it: every PNG is a full-canvas layer, id/role from filename (`03_arm_l.png` → z=3, id=arm_l).
|
|
8
|
+
* Layers are assumed to share the document canvas size unless bbox is given.
|
|
9
|
+
*/
|
|
10
|
+
export interface LayerSpec {
|
|
11
|
+
file: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
role?: string;
|
|
14
|
+
parent?: string;
|
|
15
|
+
z?: number;
|
|
16
|
+
bbox?: [number, number, number, number];
|
|
17
|
+
joint?: [number, number];
|
|
18
|
+
inpainted?: boolean;
|
|
19
|
+
prompt?: string;
|
|
20
|
+
sourceImage?: string;
|
|
21
|
+
/** swap patch (closed eyes, mouth shape): no bone of its own, slot on `parent`'s bone, hidden until an animation shows it */
|
|
22
|
+
swap?: boolean;
|
|
23
|
+
/** attach to this existing bone instead of creating one (accessory drawn with the bone, e.g. a hat on `head`) */
|
|
24
|
+
bone?: string;
|
|
25
|
+
/** slot id for swap layers; several assets with the same `slot` become one hidden slot (mouth shapes) */
|
|
26
|
+
slot?: string;
|
|
27
|
+
/** blend mode of the slot (`add` for glow/fire layers painted on black) */
|
|
28
|
+
blend?: "normal" | "add" | "multiply" | "screen";
|
|
29
|
+
}
|
|
30
|
+
export declare function pngSize(buf: Buffer): [number, number];
|
|
31
|
+
export declare function importLayers(doc: Doc, dir: string, relPrefix?: string): Promise<Doc>;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write `value` as pretty JSON to `path`, atomically. The temp file is a SIBLING — a rename across devices is
|
|
3
|
+
* neither atomic nor guaranteed to work — named so the editor watcher's basename filter ignores it (and matched
|
|
4
|
+
* by `.gitignore`'s `.*.tmp`, so a crash between the write and the rename cannot leave a tracked stray behind).
|
|
5
|
+
* A failed write removes it rather than leaving it around.
|
|
6
|
+
*/
|
|
7
|
+
/** Write `data` to `path` atomically — the temp-sibling-then-rename dance `saveJson` documents above. */
|
|
8
|
+
export declare function saveBytes(path: string, data: Buffer | string): Promise<void>;
|
|
9
|
+
export declare const saveJson: (path: string, value: unknown) => Promise<void>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { type Skeleton } from "./rig-anim";
|
|
2
|
+
import type { RigDocument, MeshAttachment, RegionAttachment, Asset } from "./rig-format";
|
|
3
|
+
export interface AlphaMask {
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
alpha: Uint8Array;
|
|
7
|
+
}
|
|
8
|
+
export type Pt = [number, number];
|
|
9
|
+
export interface TraceOptions {
|
|
10
|
+
threshold?: number;
|
|
11
|
+
tolerance?: number;
|
|
12
|
+
margin?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Every separate opaque island's outline, simplified and offset, largest first. A layer is not always one blob —
|
|
16
|
+
* a closed-eyes overlay is two strokes, a dashed outfit trim is a dozen — and meshing only the biggest one drops
|
|
17
|
+
* the rest of the image silently. Holes (a loop wound against the largest loop's orientation, e.g. the inside of
|
|
18
|
+
* a ring) are dropped, as is any island under `MIN_ISLAND_AREA` of the largest.
|
|
19
|
+
*/
|
|
20
|
+
export declare function traceHulls(mask: AlphaMask, opts?: TraceOptions): Pt[][];
|
|
21
|
+
/** the outline of the largest opaque island only — `traceHulls` when every island matters */
|
|
22
|
+
export declare function traceHull(mask: AlphaMask, opts?: TraceOptions): Pt[];
|
|
23
|
+
export declare function signedArea(p: Pt[]): number;
|
|
24
|
+
export declare function pointInPolygon(poly: Pt[], x: number, y: number): boolean;
|
|
25
|
+
export declare function distanceToPolygon(poly: Pt[], x: number, y: number): number;
|
|
26
|
+
export declare function interiorPoints(hull: Pt[], spacing: number): Pt[];
|
|
27
|
+
/** hull edges longer than `spacing` are subdivided so the outline survives Delaunay; outside triangles are culled */
|
|
28
|
+
export declare function triangulate(hull: Pt[], interior: Pt[], spacing: number): {
|
|
29
|
+
vertices: number[];
|
|
30
|
+
triangles: number[];
|
|
31
|
+
hull: number;
|
|
32
|
+
};
|
|
33
|
+
/** `minArea`: ignore a flipped triangle whose rest-pose area (px²) is below this — hairline slivers flip sign from
|
|
34
|
+
* rounding noise alone and aren't a real fold. Default 0 keeps every flip (the pre-existing behaviour). */
|
|
35
|
+
export declare function foldedTriangles(rest: number[], world: number[], triangles: number[], minArea?: number): number[];
|
|
36
|
+
/** signed area of triangle (a, b, c) of a flat [x, y, …] vertex array — negative when it winds the other way */
|
|
37
|
+
export declare const triangleArea: (v: number[], a: number, b: number, c: number) => number;
|
|
38
|
+
/** total area of a mesh's triangles in the given vertex positions — the denominator for "how much of it folded" */
|
|
39
|
+
export declare function meshArea(vertices: number[], triangles: number[]): number;
|
|
40
|
+
/**
|
|
41
|
+
* World positions of a mesh's REST shape under the given skeleton — `attachmentWorldVertices` with the pose's
|
|
42
|
+
* deform offsets left out, which is what auto-weighting and the weight brush measure distances against (a mesh
|
|
43
|
+
* is skinned to where its rest vertices are, not to where a deform key happens to have pushed them).
|
|
44
|
+
*/
|
|
45
|
+
export declare function meshWorldVertices(skel: Skeleton, slotId: string, att: MeshAttachment): number[];
|
|
46
|
+
/** 6 decimals, the precision rest shapes, weights and deform offsets are written at — what rig.json keeps
|
|
47
|
+
* (`+ 0` turns a rounded -0 back into 0). Shared with the registry tools, the editor's gizmos and verify-mesh. */
|
|
48
|
+
export declare const round6: (v: number) => number;
|
|
49
|
+
/**
|
|
50
|
+
* region → mesh over already-traced islands: each island is filled with an interior grid and triangulated on its
|
|
51
|
+
* own, then the islands are concatenated with their triangle indices offset. uvs are x/imgW, y/imgH; vertices are
|
|
52
|
+
* slot-bone local, through the region placement.
|
|
53
|
+
* `hull` (the leading vertices that form THE outline) only makes sense for a single island, so a multi-island
|
|
54
|
+
* mesh reports 0 — consumers then treat every vertex as outline, which is right when there is more than one.
|
|
55
|
+
*/
|
|
56
|
+
export declare function meshFromIslands(islands: Pt[][], region: RegionAttachment, asset: Asset, mask: {
|
|
57
|
+
width: number;
|
|
58
|
+
height: number;
|
|
59
|
+
}, spacing?: number): MeshAttachment;
|
|
60
|
+
/** region → mesh: every opaque island's outline + an interior grid, triangulated (see `traceHulls`, `meshFromIslands`) */
|
|
61
|
+
export declare function meshFromMask(mask: AlphaMask, region: RegionAttachment, asset: Asset, opts?: {
|
|
62
|
+
spacing?: number;
|
|
63
|
+
tolerance?: number;
|
|
64
|
+
margin?: number;
|
|
65
|
+
threshold?: number;
|
|
66
|
+
}): MeshAttachment;
|
|
67
|
+
/** `radius` is the falloff's distance scale, in world px — see `autoWeights` */
|
|
68
|
+
export interface AutoWeightsOptions {
|
|
69
|
+
bones: string[];
|
|
70
|
+
radius: number;
|
|
71
|
+
maxInfluences: number;
|
|
72
|
+
}
|
|
73
|
+
/** normalized weights below this are dropped rather than kept as a dead influence that rounds to 0 in the file */
|
|
74
|
+
export declare const MIN_AUTO_WEIGHT = 0.000001;
|
|
75
|
+
/** default candidates: the slot's bone, its parent, its direct children */
|
|
76
|
+
export declare function defaultWeightBones(doc: RigDocument, slotId: string): string[];
|
|
77
|
+
/**
|
|
78
|
+
* Inverse-distance skinning. For every vertex: the distance `d` to each candidate bone's segment (joint → joint +
|
|
79
|
+
* length along x; length 0 = a point), weight `1 / (d/radius + 1)²`, top-K by weight, normalized; influences hold
|
|
80
|
+
* the vertex in each bone's local space.
|
|
81
|
+
*
|
|
82
|
+
* `radius` scales the falloff: it is the distance, in world px, at which the weight has fallen to 1/4 of its
|
|
83
|
+
* value at the joint. `1` reproduces the historical law `1 / (d + 1px)²` exactly — which is why it is the
|
|
84
|
+
* default, and omitting `radius` changes nothing. A larger radius flattens the curve, so bones farther away keep
|
|
85
|
+
* a meaningful share; a smaller one sharpens it, so the nearest bone dominates within a few px.
|
|
86
|
+
*
|
|
87
|
+
* There is deliberately no cutoff at any radius: every candidate bone keeps a nonzero share, however far past
|
|
88
|
+
* `radius` it sits. A hard cutoff is what this replaced, and it was worse than useless — the middle of a limb
|
|
89
|
+
* sat beyond `radius` of both of its joints (the old default was half the mesh's extent), so it had no bone in
|
|
90
|
+
* range at all and had to snap to its single nearest bone at w=1; neighbouring vertices snapping to *different*
|
|
91
|
+
* bones is a hard seam with no blend, and it flips the triangles between them as soon as those bones
|
|
92
|
+
* counter-rotate.
|
|
93
|
+
*/
|
|
94
|
+
export declare function autoWeights(doc: RigDocument, skel: Skeleton, att: MeshAttachment, opts: AutoWeightsOptions): MeshAttachment;
|
|
95
|
+
/**
|
|
96
|
+
* What the fitting and deform math below actually reads: the slot the points live in, plus the `VertexData` a
|
|
97
|
+
* mesh and a PATH carry identically (`vertices`, or `weights` skinned to bones). Neither cares which of the two
|
|
98
|
+
* it was handed — a path attachment is skinned exactly like a mesh, vertex for vertex.
|
|
99
|
+
*/
|
|
100
|
+
export type VertexAttachment = Pick<MeshAttachment, "slot" | "vertices" | "weights">;
|
|
101
|
+
/** one rest shape, never both — what a vertex gesture commits and what `rig_set_mesh` / `rig_set_path_attachment` take */
|
|
102
|
+
export type VertexPatch = {
|
|
103
|
+
vertices: number[];
|
|
104
|
+
weights?: undefined;
|
|
105
|
+
} | {
|
|
106
|
+
weights: NonNullable<MeshAttachment["weights"]>;
|
|
107
|
+
vertices?: undefined;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* How many CONTROL POINTS a mesh or a path holds: one influence list each when skinned, two numbers of
|
|
111
|
+
* `vertices` each when not. The same count for both shapes, in one place — the gizmo, the anchor tool, the tree's
|
|
112
|
+
* delete and the properties panel all ask it, and a copy that got the weighted branch wrong would read half a
|
|
113
|
+
* skinned path as a whole one.
|
|
114
|
+
*/
|
|
115
|
+
export declare const vertexCount: (att: Pick<VertexAttachment, "vertices" | "weights">) => number;
|
|
116
|
+
/**
|
|
117
|
+
* New local data so the attachment's vertices sit at `targetWorld` (flat, all vertices) under `skel`:
|
|
118
|
+
* `vertices` → slot-bone local; `weights` → every influence's x,y re-expressed in its bone.
|
|
119
|
+
*
|
|
120
|
+
* @deprecated Not the way to MOVE a vertex — `deformOffsetsFor` plus `VertexGizmo.shift` is, and this has no
|
|
121
|
+
* caller left in the project. Kept for a future "normalize this attachment's vertices to these positions", which
|
|
122
|
+
* is what it actually is; every gesture that reached for it collapsed the per-bone spread instead.
|
|
123
|
+
*
|
|
124
|
+
* A skinned vertex's influences do not
|
|
125
|
+
* generally agree on where it is (Spine's rarely do, and `blendInfluences` disagrees on purpose), and re-deriving
|
|
126
|
+
* them all from one world point throws that spread away for a position that only matches in the pose `skel` is
|
|
127
|
+
* in — measured at 489 px of animated drift on `ross`'s `n_fur` for a 0.01 px setup nudge.
|
|
128
|
+
*/
|
|
129
|
+
export declare function fitVertices(skel: Skeleton, att: VertexAttachment, targetWorld: number[]): VertexPatch;
|
|
130
|
+
/** flat offset length for an attachment: 2·n unweighted, 2·Σ influences weighted */
|
|
131
|
+
export declare const deformLength: (att: VertexAttachment) => number;
|
|
132
|
+
/** deform offsets (format of the `vertices` key) that move vertex `index` by `worldDelta`, starting from `base` (or zeros) */
|
|
133
|
+
export declare function deformOffsetsFor(skel: Skeleton, att: VertexAttachment, base: number[] | undefined, index: number, worldDelta: [number, number]): number[];
|
|
134
|
+
export interface BrushOptions {
|
|
135
|
+
bone: string;
|
|
136
|
+
center: [number, number];
|
|
137
|
+
radius: number;
|
|
138
|
+
strength: number;
|
|
139
|
+
maxInfluences: number;
|
|
140
|
+
slotBone: string;
|
|
141
|
+
}
|
|
142
|
+
/** add strength·(1 − d/radius) of `bone` to vertices within radius (negative strength removes), renormalize, keep top-K; a vertex left without influences gets the slot bone at 1 */
|
|
143
|
+
export declare function paintWeights(skel: Skeleton, att: MeshAttachment, world: number[], opts: BrushOptions): MeshAttachment;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rig-path — pure geometry for path attachments: splitting a curve without changing its shape, blending
|
|
3
|
+
* influence lists, curve lengths and hit-testing. Shared by the registry tools and the editor. No node:*, no DOM.
|
|
4
|
+
*
|
|
5
|
+
* Control-point layout, fixed by the format and verified against spine-core: vertex i*3+1 is anchor i, its
|
|
6
|
+
* triple is [cPrev, p, cNext], and curve i runs p_i → cNext_i → cPrev_{i+1} → p_{i+1}. The index wrapping a
|
|
7
|
+
* CLOSED path needs is taken from `samplePath` (rig-anim.ts) rather than derived again here: that sampler is
|
|
8
|
+
* pinned against the spine-core runtime, so it is what these control points mean.
|
|
9
|
+
*/
|
|
10
|
+
import type { Pt } from "./rig-constraints";
|
|
11
|
+
import type { Influence } from "./rig-format";
|
|
12
|
+
/** flat [x, y, …] ↔ point pairs — the format stores flat arrays, the geometry below reads points */
|
|
13
|
+
export declare const toPoints: (flat: number[]) => Pt[];
|
|
14
|
+
export declare const flatten: (pts: Pt[]) => number[];
|
|
15
|
+
/** anchors of a path: the vertices at 1, 4, 7, … (`vertexCount` counts POINTS, not numbers) */
|
|
16
|
+
export declare const anchorCount: (vertexCount: number) => number;
|
|
17
|
+
/** curves of a path: one per anchor when closed (the last wraps), one fewer when open */
|
|
18
|
+
export declare const curveCount: (vertexCount: number, closed: boolean) => number;
|
|
19
|
+
/** point on the cubic bezier through p0..p3 at parameter t */
|
|
20
|
+
export declare function bezierPointAt(p0: Pt, p1: Pt, p2: Pt, p3: Pt, t: number): Pt;
|
|
21
|
+
/** cubic bezier length, approximated by 8 chords (the curve bulges outward, so this runs a few % longer than the straight p0→p3 chord) */
|
|
22
|
+
export declare function bezierLength(p0: Pt, p1: Pt, p2: Pt, p3: Pt, chords?: number): number;
|
|
23
|
+
/** the point of curve `i` at parameter `t`, in whatever space `worldPoints` are given in (same wrapping as `samplePath`) */
|
|
24
|
+
export declare const pathPointAt: (worldPoints: number[], i: number, t: number) => Pt;
|
|
25
|
+
/**
|
|
26
|
+
* Cumulative curve lengths of a path in the space its points are given in — one entry per curve, which is the
|
|
27
|
+
* convention `pathFromPoints` writes. (A Spine-imported OPEN path carries one entry more; `pathLength` reads
|
|
28
|
+
* `lengths` by curve index, so shortening to one-per-curve loses nothing, and with `constantSpeed` on the
|
|
29
|
+
* runtime measures the posed curve and never reads these at all.)
|
|
30
|
+
*/
|
|
31
|
+
export declare function curveLengths(worldPoints: number[], closed: boolean): number[];
|
|
32
|
+
/**
|
|
33
|
+
* De Casteljau split of curve `i` at `t`: the five control points the split changes or adds, in world space.
|
|
34
|
+
* The two halves p0 → a → d → f and f → e → c → p3 are, together, the ORIGINAL curve — exactly, not to some
|
|
35
|
+
* tolerance — so inserting an anchor this way moves nothing that was already drawn.
|
|
36
|
+
*
|
|
37
|
+
* `coeffs` are the affine coefficients of those five points over the curve's own four control points
|
|
38
|
+
* `[p_i, cNext_i, cPrev_{i+1}, p_{i+1}]`, in the order `[cNextI, triple[0], triple[1], triple[2], cPrevNext]`.
|
|
39
|
+
* Each row sums to 1 and is non-negative for t in (0, 1), so a WEIGHTED path can rebuild the five influence
|
|
40
|
+
* lists from the four it already has (`blendInfluences`) and stay skinned to the same bones.
|
|
41
|
+
*/
|
|
42
|
+
export declare function splitCurve(worldPoints: number[], closed: boolean, i: number, t: number): {
|
|
43
|
+
cNextI: Pt;
|
|
44
|
+
triple: [Pt, Pt, Pt];
|
|
45
|
+
cPrevNext: Pt;
|
|
46
|
+
coeffs: number[][];
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Apply a `splitCurve` to a list with ONE ENTRY PER CONTROL POINT: `cNext_i` and `cPrev_{i+1}` are replaced
|
|
50
|
+
* (the split moves them) and the new anchor's `[cPrev, p, cNext]` triple goes in between. The point array, the
|
|
51
|
+
* local `vertices` and the `weights` lists are all this shape and must stay in step, so they share this.
|
|
52
|
+
* On a closed path the last curve's `cPrev_{i+1}` is entry 0 and the triple appends at the end — the same
|
|
53
|
+
* wrapping `curveOf` reads the curve with.
|
|
54
|
+
*/
|
|
55
|
+
export declare function insertTriple<T>(items: T[], i: number, cNextI: T, triple: [T, T, T], cPrevNext: T): T[];
|
|
56
|
+
/** drop anchor `i`'s whole `[cPrev, p, cNext]` triple from a per-control-point list */
|
|
57
|
+
export declare function removeTriple<T>(items: T[], i: number): T[];
|
|
58
|
+
/**
|
|
59
|
+
* Blend skinned control points by affine coefficients: the WHOLE influence list, positions included.
|
|
60
|
+
*
|
|
61
|
+
* Under linear blend skinning a control point sits at `P_k = Σ_j w_kj · W_j(q_kj)`, so the affine combination
|
|
62
|
+
* `P' = Σ_k c_k · P_k` expands — every bone matrix `W_j` being affine — into a control point of the same form:
|
|
63
|
+
*
|
|
64
|
+
* w'_j = Σ_k c_k · w_kj q'_j = Σ_k c_k · w_kj · q_kj / w'_j
|
|
65
|
+
*
|
|
66
|
+
* That is an identity in the bone matrices, so the blended point follows `P'` in EVERY pose, which is the only
|
|
67
|
+
* thing worth having: a split anchor that is right at rest and wrong the moment the bones move has preserved
|
|
68
|
+
* nothing. Mapping the blended WORLD point back through each bone's inverse instead — what `fitVertices` does,
|
|
69
|
+
* correctly, for its own job of fitting existing influences to one target position — agrees with this only
|
|
70
|
+
* where the bones stand in their setup pose. On ross's posed tail that route drifts by ~50 px.
|
|
71
|
+
*
|
|
72
|
+
* `coeffs` must sum to 1 (affine, not linear) and each source list must already be normalized.
|
|
73
|
+
*/
|
|
74
|
+
export declare function blendInfluences(sources: Influence[][], coeffs: number[]): Influence[];
|
|
75
|
+
/**
|
|
76
|
+
* The DEFORM OFFSETS of a blended control point — the same `Σ c·w·d / w'` its rest positions get, and for the
|
|
77
|
+
* same reason. An offset adds to the stored position INSIDE each bone's frame (`attachmentWorldVertices` poses
|
|
78
|
+
* `q_kj + d_kj`), so a deformed control point is `Σ_j w_kj · W_j(q_kj + d_kj)`; running the derivation in
|
|
79
|
+
* `blendInfluences` over that instead of over `q` alone gives back the very same identity with `q + d` in place
|
|
80
|
+
* of `q`, and subtracting the rest half leaves `d'_j = Σ_k c_k·w_kj·d_kj / w'_j`. The blended anchor therefore
|
|
81
|
+
* sits on the DEFORMED curve in every pose exactly as its rest position sits on the rest curve — and, the blend
|
|
82
|
+
* being linear in `d`, at every time between two keys as well, not only on them.
|
|
83
|
+
*
|
|
84
|
+
* `sources` are the same four influence lists the positions blend from and `chunks` their offset pairs in the
|
|
85
|
+
* same order, so the result is aligned to `blendInfluences(sources, coeffs)` influence for influence: the weights
|
|
86
|
+
* decide which bones survive and in what order, and they are identical in both calls.
|
|
87
|
+
*/
|
|
88
|
+
export declare function blendOffsets(sources: Influence[][], chunks: number[][], coeffs: number[]): number[];
|
|
89
|
+
/** the same blend for an UNWEIGHTED path, where a control point is one pair in the slot bone's space */
|
|
90
|
+
export declare const blendPairs: (chunks: number[][], coeffs: number[]) => number[];
|
|
91
|
+
/**
|
|
92
|
+
* Nearest point on the POSED curve: which curve, its parameter, and the world distance — so the caller can
|
|
93
|
+
* apply its own pick threshold (a click far from the curve is a miss, not a very bad hit).
|
|
94
|
+
* 16 samples per curve locate the minimum, then 8 rounds of golden-section refine it inside the bracket its
|
|
95
|
+
* two neighbours make — down to ~1/350 of a curve, well under a pixel on any curve a user can see.
|
|
96
|
+
*/
|
|
97
|
+
export declare function nearestOnPath(worldPoints: number[], closed: boolean, p: Pt): {
|
|
98
|
+
curve: number;
|
|
99
|
+
t: number;
|
|
100
|
+
d: number;
|
|
101
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rig-presets v1 — parametric animation generators.
|
|
3
|
+
*
|
|
4
|
+
* A preset = (doc, params) → Animation. The Animation keeps `preset: {name, params}`
|
|
5
|
+
* so an agent can re-tune params instead of editing keys.
|
|
6
|
+
* Presets pick bones by `role`; missing roles are skipped, root is the fallback.
|
|
7
|
+
* Image deformation (rig-deform.ts) goes through `deform` tracks on slots: breathing bulges the torso
|
|
8
|
+
* image with the neck and waist pinned instead of scaling the torso bone (which stretched children).
|
|
9
|
+
*/
|
|
10
|
+
import type { RigDocument, Animation } from "./rig-format";
|
|
11
|
+
export interface Preset<P> {
|
|
12
|
+
name: string;
|
|
13
|
+
defaults: P;
|
|
14
|
+
build(doc: RigDocument, params: P): Animation;
|
|
15
|
+
}
|
|
16
|
+
export interface IdleBreathingParams {
|
|
17
|
+
id: string;
|
|
18
|
+
period: number;
|
|
19
|
+
amplitude: number;
|
|
20
|
+
headBob: number;
|
|
21
|
+
armSway: number;
|
|
22
|
+
}
|
|
23
|
+
export declare const idleBreathing: Preset<IdleBreathingParams>;
|
|
24
|
+
export interface WinBounceParams {
|
|
25
|
+
id: string;
|
|
26
|
+
duration: number;
|
|
27
|
+
height: number;
|
|
28
|
+
bounces: number;
|
|
29
|
+
squash: number;
|
|
30
|
+
}
|
|
31
|
+
export declare const winBounce: Preset<WinBounceParams>;
|
|
32
|
+
export interface AnticipationShakeParams {
|
|
33
|
+
id: string;
|
|
34
|
+
duration: number;
|
|
35
|
+
amplitude: number;
|
|
36
|
+
frequency: number;
|
|
37
|
+
grow: boolean;
|
|
38
|
+
scaleUp: number;
|
|
39
|
+
}
|
|
40
|
+
export declare const anticipationShake: Preset<AnticipationShakeParams>;
|
|
41
|
+
export interface BlinkParams {
|
|
42
|
+
id: string;
|
|
43
|
+
slot: string;
|
|
44
|
+
closed?: string;
|
|
45
|
+
period: number;
|
|
46
|
+
hold: number;
|
|
47
|
+
}
|
|
48
|
+
export declare const blink: Preset<BlinkParams>;
|
|
49
|
+
export interface MouthTalkParams {
|
|
50
|
+
id: string;
|
|
51
|
+
slot: string;
|
|
52
|
+
frames: (string | null)[];
|
|
53
|
+
rate: number;
|
|
54
|
+
duration: number;
|
|
55
|
+
}
|
|
56
|
+
export declare const mouthTalk: Preset<MouthTalkParams>;
|
|
57
|
+
export interface DimParams {
|
|
58
|
+
id: string;
|
|
59
|
+
alpha: number;
|
|
60
|
+
duration: number;
|
|
61
|
+
}
|
|
62
|
+
export declare const dim: Preset<DimParams>;
|
|
63
|
+
export interface SymbolIdleParams {
|
|
64
|
+
id: string;
|
|
65
|
+
period: number;
|
|
66
|
+
float: number;
|
|
67
|
+
pulse: number;
|
|
68
|
+
tilt: number;
|
|
69
|
+
}
|
|
70
|
+
export declare const symbolIdle: Preset<SymbolIdleParams>;
|
|
71
|
+
export interface SymbolWinParams {
|
|
72
|
+
id: string;
|
|
73
|
+
duration: number;
|
|
74
|
+
pop: number;
|
|
75
|
+
wobble: number;
|
|
76
|
+
wobbles: number;
|
|
77
|
+
}
|
|
78
|
+
export declare const symbolWin: Preset<SymbolWinParams>;
|
|
79
|
+
export interface WeightShiftParams {
|
|
80
|
+
id: string;
|
|
81
|
+
period: number;
|
|
82
|
+
shift: number;
|
|
83
|
+
lean: number;
|
|
84
|
+
dip: number;
|
|
85
|
+
}
|
|
86
|
+
/** Idle variation: hips sway sideways, torso leans against it, legs pivot so the feet stay planted (asin(shift/leg length)). */
|
|
87
|
+
export declare const weightShift: Preset<WeightShiftParams>;
|
|
88
|
+
export declare const presets: {
|
|
89
|
+
readonly idle_breathing: Preset<IdleBreathingParams>;
|
|
90
|
+
readonly win_bounce: Preset<WinBounceParams>;
|
|
91
|
+
readonly anticipation_shake: Preset<AnticipationShakeParams>;
|
|
92
|
+
readonly blink: Preset<BlinkParams>;
|
|
93
|
+
readonly mouth_talk: Preset<MouthTalkParams>;
|
|
94
|
+
readonly dim: Preset<DimParams>;
|
|
95
|
+
readonly symbol_idle: Preset<SymbolIdleParams>;
|
|
96
|
+
readonly symbol_win: Preset<SymbolWinParams>;
|
|
97
|
+
readonly weight_shift: Preset<WeightShiftParams>;
|
|
98
|
+
};
|
|
99
|
+
export type PresetName = keyof typeof presets;
|
|
100
|
+
/** Build an animation from a preset name + partial params, replacing any animation with the same id. */
|
|
101
|
+
export declare function applyPreset(doc: RigDocument, name: PresetName, params?: Record<string, unknown>): RigDocument;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rig-runtime v2 — PixiJS (v8) binding for `energy8.rig` documents.
|
|
3
|
+
*
|
|
4
|
+
* - Pose math lives in rig-anim.ts / rig-deform.ts (pure); this file only owns display objects.
|
|
5
|
+
* - One display object per attachment: region → Sprite (or a 9×9 MeshPlane when a lattice deform animates it),
|
|
6
|
+
* mesh → Mesh with its own geometry, vertices skinned on the CPU every pose (positions in document space).
|
|
7
|
+
* - All display objects live in ONE flat container sorted by draw order (slot z, or the drawOrder track).
|
|
8
|
+
* - play/tick/stop are a thin wrapper over an AnimationState (see rig-state.ts) kept for simple consumers
|
|
9
|
+
* (harness, viewer). Games should drive `state` directly for queues, priorities and overlays.
|
|
10
|
+
*/
|
|
11
|
+
import { Container, Texture } from "pixi.js";
|
|
12
|
+
import { type RigDocument, type Asset } from "./rig-format";
|
|
13
|
+
import { type Pose, type Affine } from "./rig-anim";
|
|
14
|
+
import { AnimationState, type PlayResult } from "./rig-state";
|
|
15
|
+
/** vertices per axis of a lattice-deformed region's render mesh (8 segments) */
|
|
16
|
+
export declare const MESH_GRID = 9;
|
|
17
|
+
export interface PlayOptions {
|
|
18
|
+
loop?: boolean;
|
|
19
|
+
/** crossfade seconds from current pose */
|
|
20
|
+
mix?: number;
|
|
21
|
+
onComplete?: () => void;
|
|
22
|
+
onEvent?: (name: string, payload?: Record<string, unknown>) => void;
|
|
23
|
+
}
|
|
24
|
+
/** Build the asset-id → Texture map, loading each `src` once and cutting atlas frames (with rotation and trim). */
|
|
25
|
+
export declare function createTextures(doc: RigDocument, loadSource: (src: string) => Promise<Texture>): Promise<Map<string, Texture>>;
|
|
26
|
+
/**
|
|
27
|
+
* Atlas-space uv (0..1 over the source image) for a point given in original-image fractions,
|
|
28
|
+
* honouring frame, trim and 90° packing rotation.
|
|
29
|
+
*/
|
|
30
|
+
export declare function atlasUV(asset: Asset, u: number, v: number, sourceW: number, sourceH: number): [number, number];
|
|
31
|
+
export declare class RigPlayer {
|
|
32
|
+
doc: RigDocument;
|
|
33
|
+
readonly view: Container<import("pixi.js").ContainerChild>;
|
|
34
|
+
readonly state: AnimationState;
|
|
35
|
+
private items;
|
|
36
|
+
private skel;
|
|
37
|
+
private textures;
|
|
38
|
+
private pages;
|
|
39
|
+
/** slots the editor hides; honoured by setPose */
|
|
40
|
+
private hidden;
|
|
41
|
+
/** `mix`: default crossfade seconds for state transitions (setBase, completion → base); play() passes its own */
|
|
42
|
+
constructor(doc: RigDocument, textures: Map<string, Texture>, opts?: {
|
|
43
|
+
mix?: number;
|
|
44
|
+
});
|
|
45
|
+
/** whole atlas page as a plain texture (identity uv mapping) for skinned meshes */
|
|
46
|
+
private pageTexture;
|
|
47
|
+
/** Replace whatever is playing (no queueing). Resolves when the animation completes or is replaced. */
|
|
48
|
+
play(animId: string, opts?: PlayOptions): Promise<PlayResult>;
|
|
49
|
+
/**
|
|
50
|
+
* Swap the document for pose evaluation without rebuilding display objects — valid only when slots/attachments
|
|
51
|
+
* keep their ids and types (the editor uses it during drags). Throws otherwise: rebuild the player.
|
|
52
|
+
*/
|
|
53
|
+
updateDoc(doc: RigDocument): void;
|
|
54
|
+
/** Editor-only: hide a slot's display object regardless of the pose (restored by passing `true`). */
|
|
55
|
+
setSlotVisible(slotId: string, visible: boolean): void;
|
|
56
|
+
/** Stop playback and immediately restore the complete setup pose. */
|
|
57
|
+
stop(): void;
|
|
58
|
+
/** dt in seconds */
|
|
59
|
+
tick(dt: number): void;
|
|
60
|
+
/** Apply a pose (rest where silent) to the display objects. */
|
|
61
|
+
setPose(pose: Pose): void;
|
|
62
|
+
/** World-space bone positions — used by editor gizmos and renderPreview overlays */
|
|
63
|
+
bonePositions(): Record<string, {
|
|
64
|
+
x: number;
|
|
65
|
+
y: number;
|
|
66
|
+
rotation: number;
|
|
67
|
+
}>;
|
|
68
|
+
/** current world transforms (for tests and tools) */
|
|
69
|
+
get world(): Map<string, Affine>;
|
|
70
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rig-state — animation state machine over a pose sink (RigPlayer, or anything with `setPose`).
|
|
3
|
+
*
|
|
4
|
+
* - base: the looping default (`idle`); shown whenever nothing else is active. A non-looping base holds its end pose.
|
|
5
|
+
* - main: one-shot (or forced loop) requests with priorities and a FIFO queue. Equal/higher priority interrupts,
|
|
6
|
+
* lower priority queues (or is dropped). After completion → next queued, else base, else hold the last pose.
|
|
7
|
+
* - overlays: named independent loops whose keys override the shown pose (blink, mouth). Non-looping overlays
|
|
8
|
+
* remove themselves when done.
|
|
9
|
+
* - crossfade: numbers blend from the last composed pose; discrete keys (attachment) hold until the mix ends.
|
|
10
|
+
* Every `play` promise resolves — a game can `await` it without risk of hanging.
|
|
11
|
+
*/
|
|
12
|
+
import type { RigDocument } from "./rig-format";
|
|
13
|
+
import { type Pose } from "./rig-anim";
|
|
14
|
+
export interface PoseSink {
|
|
15
|
+
doc: RigDocument;
|
|
16
|
+
setPose(pose: Pose): void;
|
|
17
|
+
}
|
|
18
|
+
export type PlayResult = "completed" | "interrupted" | "dropped" | "stopped";
|
|
19
|
+
export interface PlayOptions {
|
|
20
|
+
/** default 0; a request interrupts the current one when its priority is >= */
|
|
21
|
+
priority?: number;
|
|
22
|
+
/** default: the animation's own `loop` */
|
|
23
|
+
loop?: boolean;
|
|
24
|
+
/** crossfade seconds from the current pose; default: the state's `mix` */
|
|
25
|
+
mix?: number;
|
|
26
|
+
/** what to do when a higher-priority animation is playing (default "queue") */
|
|
27
|
+
ifBusy?: "queue" | "drop" | "interrupt";
|
|
28
|
+
/** synchronous callback, in addition to the returned promise */
|
|
29
|
+
onComplete?: (result: PlayResult) => void;
|
|
30
|
+
}
|
|
31
|
+
export declare class AnimationState {
|
|
32
|
+
private readonly sink;
|
|
33
|
+
onEvent?: (name: string, payload?: Record<string, unknown>) => void;
|
|
34
|
+
private base?;
|
|
35
|
+
private main?;
|
|
36
|
+
private queue;
|
|
37
|
+
private overlays;
|
|
38
|
+
/** last composed pose without overlays — mix snapshots start here */
|
|
39
|
+
private pose;
|
|
40
|
+
private from?;
|
|
41
|
+
private mixLeft;
|
|
42
|
+
private mixTotal;
|
|
43
|
+
/** a mix begun in this update shows weight 0 for the frame it starts on */
|
|
44
|
+
private mixFresh;
|
|
45
|
+
private readonly defaultMix;
|
|
46
|
+
constructor(sink: PoseSink, opts?: {
|
|
47
|
+
mix?: number;
|
|
48
|
+
});
|
|
49
|
+
/** id of the animation currently shown (main, else base) */
|
|
50
|
+
get current(): string | undefined;
|
|
51
|
+
get baseId(): string | undefined;
|
|
52
|
+
private channel;
|
|
53
|
+
setBase(id?: string, opts?: {
|
|
54
|
+
mix?: number;
|
|
55
|
+
}): void;
|
|
56
|
+
play(id: string, opts?: PlayOptions): Promise<PlayResult>;
|
|
57
|
+
/** Independent loop on top of the shown pose; `undefined` removes it. */
|
|
58
|
+
setOverlay(name: string, id?: string): void;
|
|
59
|
+
/** Clear everything and show the setup pose. Pending plays resolve as "stopped". */
|
|
60
|
+
stop(): void;
|
|
61
|
+
update(dt: number): void;
|
|
62
|
+
private beginMix;
|
|
63
|
+
private advance;
|
|
64
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rig-symbol — a rig as a slot symbol view for @energy8platform/game-engine (SymbolView, engine ≥ 0.43: square or rectangular cells).
|
|
3
|
+
*
|
|
4
|
+
* The interface is copied structurally (playIdle / playWin / showStatic / resize) so this package does not
|
|
5
|
+
* depend on the engine. Centred at the origin and scaled so the rig canvas fits the cell, like AnimatedSymbol.
|
|
6
|
+
* `setDim(on)` is an extra for non-winning symbols. Updated through a ticker (Ticker.shared by default).
|
|
7
|
+
*/
|
|
8
|
+
import { Container, type Texture } from "pixi.js";
|
|
9
|
+
import { RigPlayer } from "./rig-runtime";
|
|
10
|
+
import type { RigDocument } from "./rig-format";
|
|
11
|
+
export interface TickerLike {
|
|
12
|
+
add(fn: (t: {
|
|
13
|
+
deltaMS: number;
|
|
14
|
+
}) => void): unknown;
|
|
15
|
+
remove(fn: (t: {
|
|
16
|
+
deltaMS: number;
|
|
17
|
+
}) => void): unknown;
|
|
18
|
+
}
|
|
19
|
+
/** a square cell (px) or a rectangular one, as the engine's SymbolView.resize takes */
|
|
20
|
+
export type SymbolSize = number | {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
};
|
|
24
|
+
export interface RigSymbolOptions {
|
|
25
|
+
/** cell size in px: a number scales the larger canvas side to it, a rectangle fits the canvas inside it */
|
|
26
|
+
size: SymbolSize;
|
|
27
|
+
/** animation ids to use; missing ones are skipped gracefully */
|
|
28
|
+
animations?: {
|
|
29
|
+
idle?: string;
|
|
30
|
+
win?: string;
|
|
31
|
+
dim?: string;
|
|
32
|
+
};
|
|
33
|
+
/** crossfade seconds between states (default 0.15) */
|
|
34
|
+
mix?: number;
|
|
35
|
+
ticker?: TickerLike;
|
|
36
|
+
}
|
|
37
|
+
/** structural copy of the engine's SymbolView */
|
|
38
|
+
export interface SymbolViewLike extends Container {
|
|
39
|
+
playIdle?(): void;
|
|
40
|
+
playWin?(): Promise<void>;
|
|
41
|
+
showStatic?(): void;
|
|
42
|
+
resize?(size: SymbolSize): void;
|
|
43
|
+
}
|
|
44
|
+
export declare class RigSymbol extends Container implements SymbolViewLike {
|
|
45
|
+
readonly doc: RigDocument;
|
|
46
|
+
readonly player: RigPlayer;
|
|
47
|
+
private readonly names;
|
|
48
|
+
private readonly ticker;
|
|
49
|
+
private readonly onTick;
|
|
50
|
+
constructor(doc: RigDocument, textures: Map<string, Texture>, opts: RigSymbolOptions);
|
|
51
|
+
private has;
|
|
52
|
+
resize(size: SymbolSize): void;
|
|
53
|
+
playIdle(): void;
|
|
54
|
+
/** resolves when the win animation completes (or immediately if there is none / it was replaced) */
|
|
55
|
+
playWin(): Promise<void>;
|
|
56
|
+
showStatic(): void;
|
|
57
|
+
/** dim as a held base pose (non-winning symbol); off → back to idle */
|
|
58
|
+
setDim(on: boolean): void;
|
|
59
|
+
destroy(options?: Parameters<Container["destroy"]>[0]): void;
|
|
60
|
+
}
|