@weasel-js/rig 0.0.0-stage → 1.11.1
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/LICENSE +21 -0
- package/README.md +47 -2
- package/dist/icons.table.d.ts +6 -0
- package/dist/icons.table.js +24 -0
- package/dist/icons.table.js.map +1 -0
- package/dist/icons.table.json +16 -0
- package/dist/index.d.ts +207 -0
- package/dist/index.js +518 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 orochi235
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,48 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @weasel-js/rig
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The rigging tools for a weasel canvas: the bone tool, which draws and edits the
|
|
4
|
+
skeleton a node holds, and the weight tool, which paints how much each joint
|
|
5
|
+
moves the paths under that node.
|
|
6
|
+
|
|
7
|
+
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
8
|
+
scene-graph canvas kit for React. See the
|
|
9
|
+
[API reference](https://orochi235.github.io/weasel/#pkg/rig).
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @weasel-js/rig
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A rig lives in the scene. A node holds the skeleton in `data.skeleton`, its
|
|
16
|
+
joints relative to that node's box, and the paths under it hold their weights
|
|
17
|
+
in `data.skin`. Core's `useRigNode` binds a rig from that data, and this
|
|
18
|
+
package's tools edit it, one scene op per edit, so undo, save and paste treat a
|
|
19
|
+
rig like any other node data.
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { SceneCanvas, useRigNode } from '@weasel-js/core';
|
|
23
|
+
import { createRigEditState, useRigTools } from '@weasel-js/rig';
|
|
24
|
+
|
|
25
|
+
const state = useMemo(() => createRigEditState(), []);
|
|
26
|
+
const tools = useRigTools({ scene, state });
|
|
27
|
+
useRigNode({ scene, node: arm, painting: state });
|
|
28
|
+
|
|
29
|
+
<SceneCanvas scene={scene} features={['pick', 'paths']} tools={tools} />;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`useRigTools` builds every tool in `RIG_TOOLS`, keyed by id:
|
|
33
|
+
|
|
34
|
+
| Tool | Key | What it does |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `bone` | <kbd>J</kbd> | A drag on a joint moves it with the joints under it; a drag from anywhere else draws a bone from the active joint, or from the press if none is active. A click picks the active joint, and <kbd>Delete</kbd> removes it with everything under it, taking its weights out of every skin beneath the rig |
|
|
37
|
+
| `weight` | <kbd>W</kbd> | A round brush: the points under it follow the active joint more, the nearer the brush's center, and with <kbd>Alt</kbd> less. Every point shows as a dot colored by the active joint's share of it |
|
|
38
|
+
|
|
39
|
+
Both tools act on the rig the selection names: with one node selected, the
|
|
40
|
+
nearest of it and its ancestors that holds a skeleton, else that node itself,
|
|
41
|
+
so a first bone can be drawn onto it. `rig` fixes one instead. The two share a
|
|
42
|
+
`RigEditState`, so a joint picked in one is the active joint in the other; give
|
|
43
|
+
the same state to `useRigNode` as `painting` and a weight stroke bends the path
|
|
44
|
+
as it is painted. `useBoneTool` and `useWeightTool` build either tool alone.
|
|
45
|
+
|
|
46
|
+
The tools' dot sizes are the kit settings `targets.boneJointRadiusPx` and
|
|
47
|
+
`targets.weightPointRadiusPx`, which `rigKitSettingsSchema` lists for a
|
|
48
|
+
settings page.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/icons.table.json
|
|
2
|
+
var icons_table_default = {
|
|
3
|
+
namespace: "rig",
|
|
4
|
+
owner: "@weasel-js/rig",
|
|
5
|
+
icons: {
|
|
6
|
+
bone: {
|
|
7
|
+
body: '<g stroke-linecap="butt"><path d="M 5.6 14.4 L 8.9 14.7 L 15.2 4.8 L 5.3 11.1 Z"/><circle cx="4.6" cy="15.4" r="2" stroke-width="1.25"/><circle cx="15.6" cy="4.4" r="1.4" stroke-width="1.25"/></g>',
|
|
8
|
+
group: "Tools",
|
|
9
|
+
tags: []
|
|
10
|
+
},
|
|
11
|
+
weight: {
|
|
12
|
+
body: '<g stroke-linecap="butt" stroke-linejoin="miter"><circle cx="10" cy="10" r="7" stroke-width="1" stroke-dasharray="1.6 1.6"/><circle cx="10" cy="10" r="4.25" stroke-width="1.25"/><circle cx="10" cy="10" r="2" fill="currentColor" stroke="none"/></g>',
|
|
13
|
+
group: "Tools",
|
|
14
|
+
tags: []
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/icons.table.ts
|
|
20
|
+
var icons = icons_table_default;
|
|
21
|
+
|
|
22
|
+
export { icons };
|
|
23
|
+
//# sourceMappingURL=icons.table.js.map
|
|
24
|
+
//# sourceMappingURL=icons.table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/icons.table.json","../src/icons.table.ts"],"names":[],"mappings":";AAAA,IAAA,mBAAA,GAAA;AAAA,EACE,SAAA,EAAa,KAAA;AAAA,EACb,KAAA,EAAS,gBAAA;AAAA,EACT,KAAA,EAAS;AAAA,IACP,IAAA,EAAQ;AAAA,MACN,IAAA,EAAQ,sMAAA;AAAA,MACR,KAAA,EAAS,OAAA;AAAA,MACT,MAAQ;AAAC,KACX;AAAA,IACA,MAAA,EAAU;AAAA,MACR,IAAA,EAAQ,yPAAA;AAAA,MACR,KAAA,EAAS,OAAA;AAAA,MACT,MAAQ;AAAC;AACX;AAEJ,CAAA;;;ACTO,IAAM,KAAA,GAAmB","file":"icons.table.js","sourcesContent":["{\n \"namespace\": \"rig\",\n \"owner\": \"@weasel-js/rig\",\n \"icons\": {\n \"bone\": {\n \"body\": \"<g stroke-linecap=\\\"butt\\\"><path d=\\\"M 5.6 14.4 L 8.9 14.7 L 15.2 4.8 L 5.3 11.1 Z\\\"/><circle cx=\\\"4.6\\\" cy=\\\"15.4\\\" r=\\\"2\\\" stroke-width=\\\"1.25\\\"/><circle cx=\\\"15.6\\\" cy=\\\"4.4\\\" r=\\\"1.4\\\" stroke-width=\\\"1.25\\\"/></g>\",\n \"group\": \"Tools\",\n \"tags\": []\n },\n \"weight\": {\n \"body\": \"<g stroke-linecap=\\\"butt\\\" stroke-linejoin=\\\"miter\\\"><circle cx=\\\"10\\\" cy=\\\"10\\\" r=\\\"7\\\" stroke-width=\\\"1\\\" stroke-dasharray=\\\"1.6 1.6\\\"/><circle cx=\\\"10\\\" cy=\\\"10\\\" r=\\\"4.25\\\" stroke-width=\\\"1.25\\\"/><circle cx=\\\"10\\\" cy=\\\"10\\\" r=\\\"2\\\" fill=\\\"currentColor\\\" stroke=\\\"none\\\"/></g>\",\n \"group\": \"Tools\",\n \"tags\": []\n }\n }\n}\n","// GENERATED by packages/icons/scripts/gen-icons.mjs — do not edit by hand.\n\nimport type { IconTable } from '@weasel-js/icons';\nimport data from './icons.table.json';\n\n/** Every glyph this package ships, under the `rig` namespace. */\nexport const icons: IconTable = data;\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"namespace": "rig",
|
|
3
|
+
"owner": "@weasel-js/rig",
|
|
4
|
+
"icons": {
|
|
5
|
+
"bone": {
|
|
6
|
+
"body": "<g stroke-linecap=\"butt\"><path d=\"M 5.6 14.4 L 8.9 14.7 L 15.2 4.8 L 5.3 11.1 Z\"/><circle cx=\"4.6\" cy=\"15.4\" r=\"2\" stroke-width=\"1.25\"/><circle cx=\"15.6\" cy=\"4.4\" r=\"1.4\" stroke-width=\"1.25\"/></g>",
|
|
7
|
+
"group": "Tools",
|
|
8
|
+
"tags": []
|
|
9
|
+
},
|
|
10
|
+
"weight": {
|
|
11
|
+
"body": "<g stroke-linecap=\"butt\" stroke-linejoin=\"miter\"><circle cx=\"10\" cy=\"10\" r=\"7\" stroke-width=\"1\" stroke-dasharray=\"1.6 1.6\"/><circle cx=\"10\" cy=\"10\" r=\"4.25\" stroke-width=\"1.25\"/><circle cx=\"10\" cy=\"10\" r=\"2\" fill=\"currentColor\" stroke=\"none\"/></g>",
|
|
12
|
+
"group": "Tools",
|
|
13
|
+
"tags": []
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { ToolSet, PaintedSkins, NodeId, Scene, Tool, RenderLayer } from '@weasel-js/core';
|
|
2
|
+
import { Skin, Skeleton } from '@weasel-js/animator';
|
|
3
|
+
import { ToolKeybinding } from '@weasel-js/routing';
|
|
4
|
+
import { PolygonPath } from '@weasel-js/geom';
|
|
5
|
+
import * as _weasel_js_prefs from '@weasel-js/prefs';
|
|
6
|
+
|
|
7
|
+
/** The ids `useRigTools` keys its tools by. */
|
|
8
|
+
type RigToolId = 'bone' | 'weight';
|
|
9
|
+
/** The rigging tools: bones, and the weights that bind a mesh to them. */
|
|
10
|
+
declare const RIG_TOOLS: ToolSet<RigToolId>;
|
|
11
|
+
|
|
12
|
+
/** What the bone and weight tools share between gestures: the joint they act on, and the
|
|
13
|
+
* weights a stroke has painted and not yet written. */
|
|
14
|
+
interface RigEditState extends PaintedSkins {
|
|
15
|
+
/** The active joint's name, or `null`. */
|
|
16
|
+
active(): string | null;
|
|
17
|
+
setActive(joint: string | null): void;
|
|
18
|
+
/** Weights a stroke of the weight tool has painted and not yet written, by node. Empty
|
|
19
|
+
* between strokes. `useRigNode` given this state as `painting` binds them as they change. */
|
|
20
|
+
painted(): ReadonlyMap<NodeId, Skin>;
|
|
21
|
+
/** Replace the painted weights; `null` empties them. */
|
|
22
|
+
paint(next: ReadonlyMap<NodeId, Skin> | null): void;
|
|
23
|
+
/** Notified when the active joint or the painted weights change. Returns an unsubscribe. */
|
|
24
|
+
listen(fn: () => void): () => void;
|
|
25
|
+
}
|
|
26
|
+
/** A `RigEditState` with no joint active. Hand one to both tools so a joint picked with the
|
|
27
|
+
* bone tool is the one the weight tool paints. */
|
|
28
|
+
declare function createRigEditState(): RigEditState;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The rig a selection names: with one node selected, the nearest of it and its
|
|
32
|
+
* ancestors whose `data.skeleton` exists, else that node itself, so a first
|
|
33
|
+
* bone can be drawn onto it. With none or several selected, `null`.
|
|
34
|
+
*/
|
|
35
|
+
declare function rigOfSelection(scene: {
|
|
36
|
+
get(id: NodeId): {
|
|
37
|
+
parent: NodeId | null;
|
|
38
|
+
data?: unknown;
|
|
39
|
+
} | undefined;
|
|
40
|
+
}, ids: readonly NodeId[]): NodeId | null;
|
|
41
|
+
/** Options for {@link useRigTools}. */
|
|
42
|
+
interface UseRigToolsOptions {
|
|
43
|
+
/** The scene the tools edit: the one given to `<SceneCanvas>`. */
|
|
44
|
+
scene: Scene<unknown, string, unknown>;
|
|
45
|
+
/** The selection that names the rig, read when a tool acts. Default the scene's own,
|
|
46
|
+
* which is the canvas's unless `<SceneCanvas selection>` supplies another. */
|
|
47
|
+
selection?: () => readonly NodeId[];
|
|
48
|
+
/** Fixes the rig node, in place of the one the selection names. */
|
|
49
|
+
rig?: () => NodeId | null;
|
|
50
|
+
/** The joint both tools act on and the stroke being painted. Hand the same one to
|
|
51
|
+
* `useRigNode` as `painting` so a stroke shows as it is painted. Default the hook's own. */
|
|
52
|
+
state?: RigEditState;
|
|
53
|
+
/** Screen px around a joint within which a press takes it. */
|
|
54
|
+
reach?: number;
|
|
55
|
+
/** The weight tool's brush radius, in screen px. */
|
|
56
|
+
radius?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Every tool in {@link RIG_TOOLS}, keyed by id, for `<SceneCanvas tools>`: the bone and
|
|
60
|
+
* weight tools on one scene, sharing one `RigEditState`, and acting on the rig the
|
|
61
|
+
* selection names unless `rig` fixes one.
|
|
62
|
+
*/
|
|
63
|
+
declare function useRigTools(options: UseRigToolsOptions): Record<RigToolId, Tool>;
|
|
64
|
+
|
|
65
|
+
/** Options for `useBoneTool`. */
|
|
66
|
+
interface UseBoneToolOptions {
|
|
67
|
+
scene: Scene<unknown, string, unknown>;
|
|
68
|
+
/** The node whose data holds the skeleton, or will once a bone is drawn: usually read from
|
|
69
|
+
* the selection. `null` leaves the tool with nothing to edit. */
|
|
70
|
+
rig: () => NodeId | null;
|
|
71
|
+
/** Shared with `useWeightTool`, so both act on one joint. Default: the tool's own. */
|
|
72
|
+
state?: RigEditState;
|
|
73
|
+
/** Override the default keybinding (`{ key: 'J' }`). Pass `null` to omit. */
|
|
74
|
+
keybinding?: ToolKeybinding | null;
|
|
75
|
+
/** Screen px around a joint within which a press takes it. Default 10. */
|
|
76
|
+
reach?: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Draws and edits the skeleton in a node's `data.skeleton`. A drag on a joint moves it, with
|
|
80
|
+
* the joints under it; a drag from anywhere else draws a bone, from the active joint if there
|
|
81
|
+
* is one and from the press if not, and the new joint becomes the active one. A click picks
|
|
82
|
+
* the active joint or clears it, Delete removes it with everything under it, taking its
|
|
83
|
+
* weights out of every skin beneath the rig node, and Escape clears it. Each edit is one undo
|
|
84
|
+
* entry.
|
|
85
|
+
*/
|
|
86
|
+
declare function useBoneTool(options: UseBoneToolOptions): Tool;
|
|
87
|
+
|
|
88
|
+
/** A rig node and its skeleton in the scene's space, as the rig tools edit and draw it. */
|
|
89
|
+
interface PlacedRig {
|
|
90
|
+
id: NodeId;
|
|
91
|
+
skeleton: Skeleton;
|
|
92
|
+
}
|
|
93
|
+
/** The rig at node `id`, an empty skeleton for a node that has none yet, or `null` when
|
|
94
|
+
* there is no such node. */
|
|
95
|
+
declare function placedRig(scene: {
|
|
96
|
+
get(id: NodeId): {
|
|
97
|
+
pose: unknown;
|
|
98
|
+
data?: unknown;
|
|
99
|
+
} | undefined;
|
|
100
|
+
}, id: NodeId | null): PlacedRig | null;
|
|
101
|
+
|
|
102
|
+
/** A joint dot's radius in screen px, where `targets.boneJointRadiusPx` sets none. */
|
|
103
|
+
declare const JOINT_RADIUS_PX = 4.5;
|
|
104
|
+
/**
|
|
105
|
+
* A screen-space layer drawing a skeleton as bones and joints at a constant size: a line from
|
|
106
|
+
* each joint to its parent, a ring on each joint, and the active joint filled.
|
|
107
|
+
*/
|
|
108
|
+
declare function createBoneOverlay(source: {
|
|
109
|
+
id: string;
|
|
110
|
+
/** The skeleton to draw, in the scene's space, or `null` for none. */
|
|
111
|
+
skeleton(): Skeleton | null;
|
|
112
|
+
active(): string | null;
|
|
113
|
+
subscribe(fn: () => void): () => void;
|
|
114
|
+
/** Joint dot radius in screen px, read each frame. Default `JOINT_RADIUS_PX`. */
|
|
115
|
+
jointRadiusPx?: () => number | undefined;
|
|
116
|
+
}): RenderLayer<unknown>;
|
|
117
|
+
|
|
118
|
+
/** Options for `useWeightTool`. */
|
|
119
|
+
interface UseWeightToolOptions {
|
|
120
|
+
scene: Scene<unknown, string, unknown>;
|
|
121
|
+
/** The node whose data holds the skeleton; the tool paints the paths under it. */
|
|
122
|
+
rig: () => NodeId | null;
|
|
123
|
+
/** Shared with `useBoneTool`, so both act on one joint. Default: the tool's own. */
|
|
124
|
+
state?: RigEditState;
|
|
125
|
+
/** Override the default keybinding (`{ key: 'W' }`). Pass `null` to omit. */
|
|
126
|
+
keybinding?: ToolKeybinding | null;
|
|
127
|
+
/** The brush's radius in screen px. Default 24. */
|
|
128
|
+
radius?: number;
|
|
129
|
+
/** Screen px around a joint within which a click picks it. Default 10. */
|
|
130
|
+
reach?: number;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Paints how much the active joint weighs each point of the paths under a rig node, into
|
|
134
|
+
* their `data.skin`. A click on a joint makes it the active one. A drag is a round brush:
|
|
135
|
+
* points under it follow the active joint more, the nearer the brush's center, and their
|
|
136
|
+
* other joints give way; with Alt held they follow it less. A click away from any joint is
|
|
137
|
+
* one dab. Every point shows as a dot colored by the active joint's share of it. A stroke is
|
|
138
|
+
* one undo entry, and until it ends its weights are in `state.painted()`, which `useRigNode`
|
|
139
|
+
* binds when given the same `state` as `painting`.
|
|
140
|
+
*/
|
|
141
|
+
declare function useWeightTool(options: UseWeightToolOptions): Tool;
|
|
142
|
+
|
|
143
|
+
/** One path under the rig node, where it is drawn, with the weights now showing. */
|
|
144
|
+
interface WeighedPath {
|
|
145
|
+
id: NodeId;
|
|
146
|
+
path: PolygonPath;
|
|
147
|
+
skin: Skin;
|
|
148
|
+
}
|
|
149
|
+
/** What the weight tool's actions read and write, all of it the tool's own. */
|
|
150
|
+
interface WeightEnv {
|
|
151
|
+
scene(): Scene<unknown, string, unknown>;
|
|
152
|
+
placed(): PlacedRig | null;
|
|
153
|
+
state: RigEditState;
|
|
154
|
+
/** Screen px around a joint within which a click picks it. */
|
|
155
|
+
reachPx(): number;
|
|
156
|
+
/** The brush's radius in screen px. */
|
|
157
|
+
radiusPx(): number;
|
|
158
|
+
/** Weights a stroke has painted and not yet committed, by node. */
|
|
159
|
+
painted(): ReadonlyMap<NodeId, Skin>;
|
|
160
|
+
paint(next: ReadonlyMap<NodeId, Skin> | null): void;
|
|
161
|
+
}
|
|
162
|
+
/** Every path under the rig node, with what `env.painted()` holds for it in place of its
|
|
163
|
+
* `data.skin`. */
|
|
164
|
+
declare function weighedPaths(env: Pick<WeightEnv, 'scene' | 'placed' | 'painted'>): WeighedPath[];
|
|
165
|
+
|
|
166
|
+
/** A weighed point's dot radius in screen px, where `targets.weightPointRadiusPx` sets none. */
|
|
167
|
+
declare const POINT_RADIUS_PX = 3.5;
|
|
168
|
+
/**
|
|
169
|
+
* A screen-space layer drawing a dot on every point of every path under a rig, anchors and
|
|
170
|
+
* curve handles alike, colored by how much the active joint weighs it. With no joint active
|
|
171
|
+
* the dots are gray.
|
|
172
|
+
*/
|
|
173
|
+
declare function createWeightOverlay(source: {
|
|
174
|
+
id: string;
|
|
175
|
+
paths(): readonly WeighedPath[];
|
|
176
|
+
active(): string | null;
|
|
177
|
+
subscribe(fn: () => void): () => void;
|
|
178
|
+
/** Point dot radius in screen px, read each frame. Default `POINT_RADIUS_PX`. */
|
|
179
|
+
pointRadiusPx?: () => number | undefined;
|
|
180
|
+
}): RenderLayer<unknown>;
|
|
181
|
+
|
|
182
|
+
/** The rig tools' `targets` leaves: overlay dot sizes, in CSS px. */
|
|
183
|
+
interface RigKitTargetSettings {
|
|
184
|
+
boneJointRadiusPx: number;
|
|
185
|
+
weightPointRadiusPx: number;
|
|
186
|
+
}
|
|
187
|
+
declare module '@weasel-js/react' {
|
|
188
|
+
interface KitTargetSettings extends RigKitTargetSettings {
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/** The rig tools' `targets` leaves as a prefs group whose paths are the `KitSettings` paths. */
|
|
192
|
+
declare const rigKitSettingsSchema: Omit<_weasel_js_prefs.PrefGroup, "children"> & {
|
|
193
|
+
children: {
|
|
194
|
+
readonly targets: Omit<_weasel_js_prefs.PrefGroup, "children"> & {
|
|
195
|
+
children: {
|
|
196
|
+
readonly boneJointRadiusPx: _weasel_js_prefs.PrefNumber & {
|
|
197
|
+
default: number;
|
|
198
|
+
};
|
|
199
|
+
readonly weightPointRadiusPx: _weasel_js_prefs.PrefNumber & {
|
|
200
|
+
default: number;
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export { JOINT_RADIUS_PX, POINT_RADIUS_PX, type PlacedRig, RIG_TOOLS, type RigEditState, type RigKitTargetSettings, type RigToolId, type UseBoneToolOptions, type UseRigToolsOptions, type UseWeightToolOptions, type WeighedPath, type WeightEnv, createBoneOverlay, createRigEditState, createWeightOverlay, placedRig, rigKitSettingsSchema, rigOfSelection, useBoneTool, useRigTools, useWeightTool, weighedPaths };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import { toolSet, placeSkeleton, viewToTransform, worldToScreen, defineTool, dataRestPath, createSetDataOp, unplaceSkeleton, defaultCommitAdapter } from '@weasel-js/core';
|
|
2
|
+
import { useMemo, useRef } from 'react';
|
|
3
|
+
import { useLatest, useKitSettings } from '@weasel-js/react';
|
|
4
|
+
import { ActionDisabledReason } from '@weasel-js/routing';
|
|
5
|
+
import { jointPositions, skinnable, jointAt, removeJoint, addJoint, moveJoint, brushWeights, setWeights, dropSkinJoints } from '@weasel-js/animator';
|
|
6
|
+
import { linePath, ellipsePath, pxExtent } from '@weasel-js/geom';
|
|
7
|
+
import { createEmitter } from '@weasel-js/registry';
|
|
8
|
+
import { pref } from '@weasel-js/prefs';
|
|
9
|
+
|
|
10
|
+
// src/toolSets.ts
|
|
11
|
+
var RIG_TOOLS = toolSet("rig", ["bone", "weight"]);
|
|
12
|
+
var EMPTY = { joints: [] };
|
|
13
|
+
function placedRig(scene, id) {
|
|
14
|
+
const node = id === null ? void 0 : scene.get(id);
|
|
15
|
+
if (id === null || !node) return null;
|
|
16
|
+
const local = node.data?.skeleton ?? EMPTY;
|
|
17
|
+
return { id, skeleton: placeSkeleton(local, node.pose) };
|
|
18
|
+
}
|
|
19
|
+
function sceneReach(deps, px) {
|
|
20
|
+
const e = pxExtent(px, deps.view?.get().scale ?? { x: 1, y: 1 });
|
|
21
|
+
return Math.max(e.x, e.y);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/bone/boneActions.ts
|
|
25
|
+
var eligible = { capability: "edits-page" };
|
|
26
|
+
var reachOf = (env, deps) => sceneReach(deps, env.reachPx());
|
|
27
|
+
var activeIn = (env, skeleton) => {
|
|
28
|
+
const name = env.state.active();
|
|
29
|
+
return name !== null && skeleton.joints.some((j) => j.name === name) ? name : null;
|
|
30
|
+
};
|
|
31
|
+
function commitSkeleton(scene, deps, id, next, label, removed = []) {
|
|
32
|
+
const node = scene.get(id);
|
|
33
|
+
if (!node) return;
|
|
34
|
+
const ops = [createSetDataOp({
|
|
35
|
+
id,
|
|
36
|
+
from: node.data,
|
|
37
|
+
to: { ...node.data, skeleton: unplaceSkeleton(next, node.pose) },
|
|
38
|
+
label
|
|
39
|
+
})];
|
|
40
|
+
const strip = (parent) => {
|
|
41
|
+
for (const child of scene.childrenOf(parent)) {
|
|
42
|
+
const data = scene.get(child)?.data;
|
|
43
|
+
const kept = data?.skin && dropSkinJoints(data.skin, removed);
|
|
44
|
+
if (kept && kept !== data.skin) ops.push(createSetDataOp({ id: child, from: data, to: { ...data, skin: kept }, label }));
|
|
45
|
+
strip(child);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
if (removed.length > 0) strip(id);
|
|
49
|
+
if (deps.applyOps) deps.applyOps(ops, label);
|
|
50
|
+
else scene.applyBatch(ops, label, defaultCommitAdapter(scene, deps.selection?.adapterMethods));
|
|
51
|
+
}
|
|
52
|
+
function dragAction(env, editing) {
|
|
53
|
+
return {
|
|
54
|
+
id: "bone.drag",
|
|
55
|
+
label: "Bone \u2014 move a joint or draw a bone",
|
|
56
|
+
eligible,
|
|
57
|
+
requires: ["view", "selection", "applyOps"],
|
|
58
|
+
enabled: editing,
|
|
59
|
+
invoker: {
|
|
60
|
+
timing: "ongoing",
|
|
61
|
+
start: ({ deps, drag, world }) => {
|
|
62
|
+
const at = env.placed();
|
|
63
|
+
if (!at) return {};
|
|
64
|
+
const press = drag?.start ?? world;
|
|
65
|
+
const before = env.state.active();
|
|
66
|
+
const hit = jointAt(at.skeleton, press.x, press.y, reachOf(env, deps));
|
|
67
|
+
let origin = at.skeleton;
|
|
68
|
+
let name = hit;
|
|
69
|
+
if (name === null) {
|
|
70
|
+
let parent = activeIn(env, origin);
|
|
71
|
+
if (parent === null) ({ skeleton: origin, name: parent } = addJoint(origin, null, press.x, press.y));
|
|
72
|
+
({ skeleton: origin, name } = addJoint(origin, parent, press.x, press.y));
|
|
73
|
+
}
|
|
74
|
+
const joint = name;
|
|
75
|
+
const from = jointPositions(origin).get(joint);
|
|
76
|
+
env.state.setActive(joint);
|
|
77
|
+
env.show(origin);
|
|
78
|
+
return {
|
|
79
|
+
onMove: (move) => {
|
|
80
|
+
env.show(moveJoint(origin, joint, from.x + move.world.x - press.x, from.y + move.world.y - press.y));
|
|
81
|
+
},
|
|
82
|
+
// The dispatcher hands `onEnd` a context with empty deps.
|
|
83
|
+
onEnd: (_end, reason) => {
|
|
84
|
+
const next = env.shown();
|
|
85
|
+
env.show(null);
|
|
86
|
+
if (reason === "cancel" || !next) {
|
|
87
|
+
env.state.setActive(before);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
commitSkeleton(env.scene(), deps, at.id, next, hit === null ? "Add bone" : "Move joint");
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function createBoneActions(env) {
|
|
98
|
+
const editing = () => env.placed() ? true : ActionDisabledReason.NotApplicable;
|
|
99
|
+
const hasActive = () => {
|
|
100
|
+
const at = env.placed();
|
|
101
|
+
return at && activeIn(env, at.skeleton) !== null ? true : ActionDisabledReason.NotApplicable;
|
|
102
|
+
};
|
|
103
|
+
return [
|
|
104
|
+
dragAction(env, editing),
|
|
105
|
+
{
|
|
106
|
+
id: "bone.pick",
|
|
107
|
+
label: "Bone \u2014 pick the active joint",
|
|
108
|
+
eligible,
|
|
109
|
+
requires: ["view"],
|
|
110
|
+
enabled: editing,
|
|
111
|
+
invoker: {
|
|
112
|
+
timing: "immediate",
|
|
113
|
+
run: (deps, params) => {
|
|
114
|
+
const p = params;
|
|
115
|
+
const at = env.placed();
|
|
116
|
+
if (!at || p?.pressX === void 0 || p.pressY === void 0) return;
|
|
117
|
+
env.state.setActive(jointAt(at.skeleton, p.pressX, p.pressY, reachOf(env, deps)));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "bone.remove",
|
|
123
|
+
label: "Bone \u2014 remove the active joint",
|
|
124
|
+
eligible,
|
|
125
|
+
requires: ["selection", "applyOps"],
|
|
126
|
+
// Declining with no joint active leaves Delete to the ambient `delete`.
|
|
127
|
+
enabled: hasActive,
|
|
128
|
+
invoker: {
|
|
129
|
+
timing: "immediate",
|
|
130
|
+
run: (deps) => {
|
|
131
|
+
const at = env.placed();
|
|
132
|
+
const name = at && activeIn(env, at.skeleton);
|
|
133
|
+
if (!at || name == null) return;
|
|
134
|
+
const parent = at.skeleton.joints.find((j) => j.name === name).parent;
|
|
135
|
+
const { skeleton, removed } = removeJoint(at.skeleton, name);
|
|
136
|
+
commitSkeleton(env.scene(), deps, at.id, skeleton, "Remove joint", removed);
|
|
137
|
+
env.state.setActive(parent);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "bone.clear",
|
|
143
|
+
label: "Bone \u2014 clear the active joint",
|
|
144
|
+
eligible,
|
|
145
|
+
enabled: hasActive,
|
|
146
|
+
invoker: { timing: "immediate", run: () => env.state.setActive(null) }
|
|
147
|
+
}
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
var BONE = "#4f9cf7";
|
|
151
|
+
var JOINT_FILL = "#ffffff";
|
|
152
|
+
var JOINT_RADIUS_PX = 4.5;
|
|
153
|
+
var dot = (x, y, r) => ellipsePath({ x: x - r, y: y - r, width: 2 * r, height: 2 * r });
|
|
154
|
+
function createBoneOverlay(source) {
|
|
155
|
+
return {
|
|
156
|
+
id: source.id,
|
|
157
|
+
label: "Bones",
|
|
158
|
+
space: "screen",
|
|
159
|
+
subscribe: source.subscribe,
|
|
160
|
+
draw: (_data, view) => {
|
|
161
|
+
const skeleton = source.skeleton();
|
|
162
|
+
if (!skeleton || skeleton.joints.length === 0) return [];
|
|
163
|
+
const t = viewToTransform(view);
|
|
164
|
+
const at = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const [name, p] of jointPositions(skeleton)) {
|
|
166
|
+
const [x, y] = worldToScreen(p.x, p.y, t);
|
|
167
|
+
at.set(name, { x, y });
|
|
168
|
+
}
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const joint of skeleton.joints) {
|
|
171
|
+
if (joint.parent == null) continue;
|
|
172
|
+
out.push({
|
|
173
|
+
kind: "path",
|
|
174
|
+
path: linePath(at.get(joint.parent), at.get(joint.name)),
|
|
175
|
+
stroke: { paint: { color: BONE }, width: 2 }
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const active = source.active();
|
|
179
|
+
const r = source.jointRadiusPx?.() ?? JOINT_RADIUS_PX;
|
|
180
|
+
for (const [name, p] of at) {
|
|
181
|
+
out.push({
|
|
182
|
+
kind: "path",
|
|
183
|
+
path: dot(p.x, p.y, r),
|
|
184
|
+
fill: { paint: { kind: "solid", color: name === active ? BONE : JOINT_FILL } },
|
|
185
|
+
stroke: { paint: { color: BONE }, width: 1.5 }
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
var NONE = /* @__PURE__ */ new Map();
|
|
193
|
+
function createRigEditState() {
|
|
194
|
+
let active = null;
|
|
195
|
+
let painted = NONE;
|
|
196
|
+
const changed = createEmitter();
|
|
197
|
+
return {
|
|
198
|
+
active: () => active,
|
|
199
|
+
setActive(joint) {
|
|
200
|
+
if (joint === active) return;
|
|
201
|
+
active = joint;
|
|
202
|
+
changed.emit();
|
|
203
|
+
},
|
|
204
|
+
painted: () => painted,
|
|
205
|
+
paint(next) {
|
|
206
|
+
if ((next ?? NONE) === painted) return;
|
|
207
|
+
painted = next ?? NONE;
|
|
208
|
+
changed.emit();
|
|
209
|
+
},
|
|
210
|
+
listen: changed.subscribe
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function useBoneTool(options) {
|
|
214
|
+
const ownState = useMemo(() => createRigEditState(), []);
|
|
215
|
+
const state = options.state ?? ownState;
|
|
216
|
+
const opts = useLatest({ scene: options.scene, rig: options.rig, reach: options.reach ?? 10 });
|
|
217
|
+
const liveRef = useRef(null);
|
|
218
|
+
const env = useMemo(() => {
|
|
219
|
+
const changed = createEmitter();
|
|
220
|
+
const tool = {
|
|
221
|
+
state,
|
|
222
|
+
scene: () => opts.current.scene,
|
|
223
|
+
reachPx: () => opts.current.reach,
|
|
224
|
+
placed: () => placedRig(opts.current.scene, opts.current.rig()),
|
|
225
|
+
shown: () => liveRef.current,
|
|
226
|
+
show(skeleton) {
|
|
227
|
+
liveRef.current = skeleton;
|
|
228
|
+
changed.emit();
|
|
229
|
+
},
|
|
230
|
+
listen(fn) {
|
|
231
|
+
const offOwn = changed.subscribe(fn);
|
|
232
|
+
const off = state.listen(fn);
|
|
233
|
+
return () => {
|
|
234
|
+
offOwn();
|
|
235
|
+
off();
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
return tool;
|
|
240
|
+
}, [opts, state]);
|
|
241
|
+
const actions = useMemo(() => createBoneActions(env), [env]);
|
|
242
|
+
const kitSettings = useKitSettings();
|
|
243
|
+
const overlay = useMemo(() => createBoneOverlay({
|
|
244
|
+
id: "bone-tool-skeleton",
|
|
245
|
+
skeleton: () => env.shown() ?? env.placed()?.skeleton ?? null,
|
|
246
|
+
active: state.active,
|
|
247
|
+
subscribe: env.listen,
|
|
248
|
+
jointRadiusPx: () => kitSettings.get("targets.boneJointRadiusPx")
|
|
249
|
+
}), [env, state, kitSettings]);
|
|
250
|
+
const { keybinding } = options;
|
|
251
|
+
return useMemo(() => defineTool({
|
|
252
|
+
id: "bone",
|
|
253
|
+
capabilities: ["edits-page"],
|
|
254
|
+
hookName: "useBoneTool",
|
|
255
|
+
cursor: "crosshair",
|
|
256
|
+
presentation: { label: "Bone", icon: (
|
|
257
|
+
/* @icon */
|
|
258
|
+
"rig:bone"
|
|
259
|
+
), group: "shape" },
|
|
260
|
+
...keybinding === null ? {} : { keybinding: keybinding ?? { key: "J" } },
|
|
261
|
+
actions,
|
|
262
|
+
overlay,
|
|
263
|
+
onDeactivate: () => env.show(null),
|
|
264
|
+
bindings: [
|
|
265
|
+
{ spec: { kind: "click", mods: { shift: "optional", alt: "optional" } }, actionId: "bone.pick" },
|
|
266
|
+
{ spec: { kind: "drag", mods: { shift: "optional", alt: "optional" } }, actionId: "bone.drag" },
|
|
267
|
+
{ spec: { kind: "key", key: ["Delete", "Backspace"] }, actionId: "bone.remove" },
|
|
268
|
+
{ spec: { kind: "key", key: "Escape" }, actionId: "bone.clear" }
|
|
269
|
+
]
|
|
270
|
+
}), [actions, overlay, keybinding, env]);
|
|
271
|
+
}
|
|
272
|
+
function weighedPaths(env) {
|
|
273
|
+
const rig = env.placed();
|
|
274
|
+
if (!rig) return [];
|
|
275
|
+
const scene = env.scene();
|
|
276
|
+
const painted = env.painted();
|
|
277
|
+
const out = [];
|
|
278
|
+
const walk = (parent) => {
|
|
279
|
+
for (const id of scene.childrenOf(parent)) {
|
|
280
|
+
const node = scene.get(id);
|
|
281
|
+
const rest = node && dataRestPath(node);
|
|
282
|
+
if (node && rest) {
|
|
283
|
+
const skin = painted.get(id) ?? node.data?.skin ?? {};
|
|
284
|
+
out.push({ id, path: skinnable(rest), skin });
|
|
285
|
+
}
|
|
286
|
+
walk(id);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
walk(rig.id);
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
var eligible2 = { capability: "edits-page" };
|
|
293
|
+
var activeJoint = (env) => {
|
|
294
|
+
const name = env.state.active();
|
|
295
|
+
return name !== null && env.placed()?.skeleton.joints.some((j) => j.name === name) ? name : null;
|
|
296
|
+
};
|
|
297
|
+
function dab(env, deps, joint, x, y, mode) {
|
|
298
|
+
const radius = sceneReach(deps, env.radiusPx());
|
|
299
|
+
let next = null;
|
|
300
|
+
for (const { id, path, skin } of weighedPaths(env)) {
|
|
301
|
+
const hits = brushWeights(path.coords, x, y, radius, skin[joint], mode);
|
|
302
|
+
if (hits.size === 0) continue;
|
|
303
|
+
next ??= new Map(env.painted());
|
|
304
|
+
next.set(id, setWeights(skin, path.coords.length >> 1, joint, hits));
|
|
305
|
+
}
|
|
306
|
+
return next;
|
|
307
|
+
}
|
|
308
|
+
function commitPainted(env, deps) {
|
|
309
|
+
const scene = env.scene();
|
|
310
|
+
const ops = [];
|
|
311
|
+
for (const [id, skin] of env.painted()) {
|
|
312
|
+
const data = scene.get(id)?.data;
|
|
313
|
+
if (data !== void 0) ops.push(createSetDataOp({ id, from: data, to: { ...data, skin }, label: "Paint weights" }));
|
|
314
|
+
}
|
|
315
|
+
if (ops.length > 0) {
|
|
316
|
+
if (deps.applyOps) deps.applyOps(ops, "Paint weights");
|
|
317
|
+
else scene.applyBatch(ops, "Paint weights", defaultCommitAdapter(scene, deps.selection?.adapterMethods));
|
|
318
|
+
}
|
|
319
|
+
env.paint(null);
|
|
320
|
+
}
|
|
321
|
+
function createWeightActions(env) {
|
|
322
|
+
const editing = () => env.placed() ? true : ActionDisabledReason.NotApplicable;
|
|
323
|
+
return [
|
|
324
|
+
{
|
|
325
|
+
id: "weight.paint",
|
|
326
|
+
label: "Weight \u2014 paint the active joint's weights",
|
|
327
|
+
eligible: eligible2,
|
|
328
|
+
requires: ["view", "selection", "applyOps"],
|
|
329
|
+
enabled: () => activeJoint(env) !== null ? true : ActionDisabledReason.NotApplicable,
|
|
330
|
+
invoker: {
|
|
331
|
+
timing: "ongoing",
|
|
332
|
+
start: ({ deps, drag, world, modifiers }) => {
|
|
333
|
+
const joint = activeJoint(env);
|
|
334
|
+
if (joint === null) return {};
|
|
335
|
+
const stroke = (x, y, alt) => {
|
|
336
|
+
const next = dab(env, deps, joint, x, y, alt ? "remove" : "add");
|
|
337
|
+
if (next) env.paint(next);
|
|
338
|
+
};
|
|
339
|
+
const press = drag?.start ?? world;
|
|
340
|
+
stroke(press.x, press.y, modifiers.alt);
|
|
341
|
+
return {
|
|
342
|
+
onMove: (move) => stroke(move.world.x, move.world.y, move.modifiers.alt),
|
|
343
|
+
// The dispatcher hands `onEnd` a context with empty deps.
|
|
344
|
+
onEnd: (_end, reason) => {
|
|
345
|
+
if (reason === "cancel") env.paint(null);
|
|
346
|
+
else commitPainted(env, deps);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
id: "weight.click",
|
|
354
|
+
label: "Weight \u2014 pick a joint, or dab its weight",
|
|
355
|
+
eligible: eligible2,
|
|
356
|
+
requires: ["view", "selection", "applyOps"],
|
|
357
|
+
enabled: editing,
|
|
358
|
+
invoker: {
|
|
359
|
+
timing: "immediate",
|
|
360
|
+
run: (deps, params) => {
|
|
361
|
+
const p = params;
|
|
362
|
+
const rig = env.placed();
|
|
363
|
+
if (!rig || p?.pressX === void 0 || p.pressY === void 0) return;
|
|
364
|
+
const hit = jointAt(rig.skeleton, p.pressX, p.pressY, sceneReach(deps, env.reachPx()));
|
|
365
|
+
if (hit !== null) {
|
|
366
|
+
env.state.setActive(hit);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const joint = activeJoint(env);
|
|
370
|
+
const next = joint === null ? null : dab(env, deps, joint, p.pressX, p.pressY, p.mods?.alt ? "remove" : "add");
|
|
371
|
+
if (!next) return;
|
|
372
|
+
env.paint(next);
|
|
373
|
+
commitPainted(env, deps);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
var POINT_RADIUS_PX = 3.5;
|
|
380
|
+
var RIM = "#10161d";
|
|
381
|
+
var COLD = [31, 111, 139];
|
|
382
|
+
var HOT = [255, 138, 61];
|
|
383
|
+
var IDLE = "#8a94a3";
|
|
384
|
+
var mix = (w) => {
|
|
385
|
+
const c = COLD.map((v, k) => Math.round(v + (HOT[k] - v) * w));
|
|
386
|
+
return `rgb(${c[0]}, ${c[1]}, ${c[2]})`;
|
|
387
|
+
};
|
|
388
|
+
function createWeightOverlay(source) {
|
|
389
|
+
return {
|
|
390
|
+
id: source.id,
|
|
391
|
+
label: "Weights",
|
|
392
|
+
space: "screen",
|
|
393
|
+
subscribe: source.subscribe,
|
|
394
|
+
draw: (_data, view) => {
|
|
395
|
+
const t = viewToTransform(view);
|
|
396
|
+
const active = source.active();
|
|
397
|
+
const r = source.pointRadiusPx?.() ?? POINT_RADIUS_PX;
|
|
398
|
+
const out = [];
|
|
399
|
+
for (const { path, skin } of source.paths()) {
|
|
400
|
+
const weights = active === null ? void 0 : skin[active];
|
|
401
|
+
let total;
|
|
402
|
+
if (active !== null) {
|
|
403
|
+
total = new Array(path.coords.length >> 1).fill(0);
|
|
404
|
+
for (const joint in skin) skin[joint].forEach((w, i) => {
|
|
405
|
+
if (i < total.length) total[i] += w;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
for (let i = 0; 2 * i + 1 < path.coords.length; i++) {
|
|
409
|
+
const [x, y] = worldToScreen(path.coords[2 * i], path.coords[2 * i + 1], t);
|
|
410
|
+
const share = total && total[i] > 0 ? (weights?.[i] ?? 0) / total[i] : 0;
|
|
411
|
+
out.push({
|
|
412
|
+
kind: "path",
|
|
413
|
+
path: ellipsePath({ x: x - r, y: y - r, width: 2 * r, height: 2 * r }),
|
|
414
|
+
fill: { paint: { kind: "solid", color: active === null ? IDLE : mix(share) } },
|
|
415
|
+
stroke: { paint: { color: RIM }, width: 1 }
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/weight/useWeightTool.ts
|
|
425
|
+
function useWeightTool(options) {
|
|
426
|
+
const ownState = useMemo(() => createRigEditState(), []);
|
|
427
|
+
const state = options.state ?? ownState;
|
|
428
|
+
const opts = useLatest({
|
|
429
|
+
scene: options.scene,
|
|
430
|
+
rig: options.rig,
|
|
431
|
+
radius: options.radius ?? 24,
|
|
432
|
+
reach: options.reach ?? 10
|
|
433
|
+
});
|
|
434
|
+
const env = useMemo(() => ({
|
|
435
|
+
state,
|
|
436
|
+
scene: () => opts.current.scene,
|
|
437
|
+
placed: () => placedRig(opts.current.scene, opts.current.rig()),
|
|
438
|
+
reachPx: () => opts.current.reach,
|
|
439
|
+
radiusPx: () => opts.current.radius,
|
|
440
|
+
painted: state.painted,
|
|
441
|
+
paint: state.paint
|
|
442
|
+
}), [opts, state]);
|
|
443
|
+
const actions = useMemo(() => createWeightActions(env), [env]);
|
|
444
|
+
const kitSettings = useKitSettings();
|
|
445
|
+
const overlay = useMemo(() => [
|
|
446
|
+
createWeightOverlay({
|
|
447
|
+
id: "weight-tool-points",
|
|
448
|
+
paths: () => weighedPaths(env),
|
|
449
|
+
active: state.active,
|
|
450
|
+
subscribe: state.listen,
|
|
451
|
+
pointRadiusPx: () => kitSettings.get("targets.weightPointRadiusPx")
|
|
452
|
+
}),
|
|
453
|
+
createBoneOverlay({
|
|
454
|
+
id: "weight-tool-skeleton",
|
|
455
|
+
skeleton: () => env.placed()?.skeleton ?? null,
|
|
456
|
+
active: state.active,
|
|
457
|
+
subscribe: state.listen,
|
|
458
|
+
jointRadiusPx: () => kitSettings.get("targets.boneJointRadiusPx")
|
|
459
|
+
})
|
|
460
|
+
], [env, state, kitSettings]);
|
|
461
|
+
const { keybinding } = options;
|
|
462
|
+
return useMemo(() => defineTool({
|
|
463
|
+
id: "weight",
|
|
464
|
+
capabilities: ["edits-page"],
|
|
465
|
+
hookName: "useWeightTool",
|
|
466
|
+
cursor: "crosshair",
|
|
467
|
+
presentation: { label: "Weight", icon: (
|
|
468
|
+
/* @icon */
|
|
469
|
+
"rig:weight"
|
|
470
|
+
), group: "shape" },
|
|
471
|
+
...keybinding === null ? {} : { keybinding: keybinding ?? { key: "W" } },
|
|
472
|
+
actions,
|
|
473
|
+
overlay,
|
|
474
|
+
onDeactivate: () => env.paint(null),
|
|
475
|
+
bindings: [
|
|
476
|
+
{ spec: { kind: "click", mods: { shift: "optional", alt: "optional" } }, actionId: "weight.click" },
|
|
477
|
+
{ spec: { kind: "drag", mods: { shift: "optional", alt: "optional" } }, actionId: "weight.paint" }
|
|
478
|
+
]
|
|
479
|
+
}), [actions, overlay, keybinding, env]);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/useRigTools.ts
|
|
483
|
+
function rigOfSelection(scene, ids) {
|
|
484
|
+
if (ids.length !== 1) return null;
|
|
485
|
+
const own = ids[0];
|
|
486
|
+
for (let id = own; id !== null; ) {
|
|
487
|
+
const node = scene.get(id);
|
|
488
|
+
if (!node) break;
|
|
489
|
+
if (node.data?.skeleton) return id;
|
|
490
|
+
id = node.parent;
|
|
491
|
+
}
|
|
492
|
+
return scene.get(own) ? own : null;
|
|
493
|
+
}
|
|
494
|
+
function useRigTools(options) {
|
|
495
|
+
const { scene } = options;
|
|
496
|
+
const latest = useLatest({ scene, selection: options.selection, rig: options.rig });
|
|
497
|
+
const rig = useMemo(() => () => {
|
|
498
|
+
const { scene: s, selection, rig: fixed } = latest.current;
|
|
499
|
+
return fixed ? fixed() : rigOfSelection(s, selection ? selection() : s.getSelection());
|
|
500
|
+
}, [latest]);
|
|
501
|
+
const ownState = useMemo(() => createRigEditState(), []);
|
|
502
|
+
const state = options.state ?? ownState;
|
|
503
|
+
const { reach, radius } = options;
|
|
504
|
+
const bone = useBoneTool({ scene, rig, state, reach });
|
|
505
|
+
const weight = useWeightTool({ scene, rig, state, reach, radius });
|
|
506
|
+
return useMemo(() => ({ bone, weight }), [bone, weight]);
|
|
507
|
+
}
|
|
508
|
+
var pxLeaf = (name, def) => pref.number(name, def, { unit: { scale: 1, suffix: "px" }, min: 1, max: 16, step: 0.5 });
|
|
509
|
+
var rigKitSettingsSchema = pref.group("Rig", {
|
|
510
|
+
targets: pref.group("Targets", {
|
|
511
|
+
boneJointRadiusPx: pxLeaf("Bone joint dot", JOINT_RADIUS_PX),
|
|
512
|
+
weightPointRadiusPx: pxLeaf("Weight point dot", POINT_RADIUS_PX)
|
|
513
|
+
})
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
export { JOINT_RADIUS_PX, POINT_RADIUS_PX, RIG_TOOLS, createBoneOverlay, createRigEditState, createWeightOverlay, placedRig, rigKitSettingsSchema, rigOfSelection, useBoneTool, useRigTools, useWeightTool, weighedPaths };
|
|
517
|
+
//# sourceMappingURL=index.js.map
|
|
518
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/toolSets.ts","../src/bone/placedRig.ts","../src/bone/boneActions.ts","../src/bone/boneOverlay.ts","../src/bone/rigEditState.ts","../src/bone/useBoneTool.ts","../src/weight/weightActions.ts","../src/weight/weightOverlay.ts","../src/weight/useWeightTool.ts","../src/useRigTools.ts","../src/kitSettings.ts"],"names":["jointPositions","createEmitter","eligible","createSetDataOp","defaultCommitAdapter","ActionDisabledReason","jointAt","viewToTransform","worldToScreen","ellipsePath","useMemo","useLatest","useKitSettings","defineTool"],"mappings":"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","file":"index.js","sourcesContent":["import { toolSet, type ToolSet } from '@weasel-js/core';\n\n/** The ids `useRigTools` keys its tools by. */\nexport type RigToolId = 'bone' | 'weight';\n\n/** The rigging tools: bones, and the weights that bind a mesh to them. */\nexport const RIG_TOOLS: ToolSet<RigToolId> = toolSet('rig', ['bone', 'weight']);\n","import { pxExtent } from '@weasel-js/geom';\nimport type { ActionDeps } from '@weasel-js/routing';\nimport type { NodeId } from '@weasel-js/core';\nimport { placeSkeleton, type RigNodeData } from '@weasel-js/core';\nimport type { Skeleton } from '@weasel-js/animator';\n\nconst EMPTY: Skeleton = { joints: [] };\n\n/** A rig node and its skeleton in the scene's space, as the rig tools edit and draw it. */\nexport interface PlacedRig {\n id: NodeId;\n skeleton: Skeleton;\n}\n\n/** The rig at node `id`, an empty skeleton for a node that has none yet, or `null` when\n * there is no such node. */\nexport function placedRig(\n scene: { get(id: NodeId): { pose: unknown; data?: unknown } | undefined },\n id: NodeId | null,\n): PlacedRig | null {\n const node = id === null ? undefined : scene.get(id);\n if (id === null || !node) return null;\n const local = (node.data as RigNodeData | null | undefined)?.skeleton ?? EMPTY;\n return { id, skeleton: placeSkeleton(local, node.pose) };\n}\n\n/** `px` screen pixels as a distance in the scene, at the view's current scale. */\nexport function sceneReach(deps: ActionDeps, px: number): number {\n const e = pxExtent(px, deps.view?.get().scale ?? { x: 1, y: 1 });\n return Math.max(e.x, e.y);\n}\n","import type { Action, ActionDeps, InvocationCtx } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\nimport type { Op } from '@weasel-js/history';\nimport { createSetDataOp } from '@weasel-js/core';\nimport type { NodeId, Scene } from '@weasel-js/core';\nimport { defaultCommitAdapter } from '@weasel-js/core';\nimport { unplaceSkeleton, type RigNodeData } from '@weasel-js/core';\nimport { addJoint, dropSkinJoints, jointAt, jointPositions, moveJoint, removeJoint } from '@weasel-js/animator';\nimport type { Skeleton } from '@weasel-js/animator';\nimport { sceneReach, type PlacedRig } from './placedRig';\nimport type { RigEditState } from './rigEditState';\n\n/** What the bone tool's actions read and write, all of it the tool's own. */\nexport interface BoneEnv {\n scene(): Scene<unknown, string, unknown>;\n /** The rig node and its skeleton in the scene's space, or `null` with nothing to edit. */\n placed(): PlacedRig | null;\n state: RigEditState;\n /** Screen px around a joint within which a press takes it. */\n reachPx(): number;\n /** Show `skeleton` in place of the document's while a drag shapes it; `null` ends that. */\n show(skeleton: Skeleton | null): void;\n shown(): Skeleton | null;\n}\n\nconst eligible = { capability: 'edits-page' } as const;\n\nconst reachOf = (env: BoneEnv, deps: ActionDeps): number => sceneReach(deps, env.reachPx());\n\nconst activeIn = (env: BoneEnv, skeleton: Skeleton): string | null => {\n const name = env.state.active();\n return name !== null && skeleton.joints.some((j) => j.name === name) ? name : null;\n};\n\n/** Write `next`, a skeleton in the scene's space, into the rig node, and take `removed`\n * joints out of the skins under it, as one undo entry. */\nfunction commitSkeleton(\n scene: Scene<unknown, string, unknown>,\n deps: ActionDeps,\n id: NodeId,\n next: Skeleton,\n label: string,\n removed: readonly string[] = [],\n): void {\n const node = scene.get(id);\n if (!node) return;\n const ops: Op[] = [createSetDataOp({\n id, from: node.data, to: { ...(node.data as object), skeleton: unplaceSkeleton(next, node.pose) }, label,\n })];\n const strip = (parent: NodeId): void => {\n for (const child of scene.childrenOf(parent)) {\n const data = scene.get(child)?.data as RigNodeData | null | undefined;\n const kept = data?.skin && dropSkinJoints(data.skin, removed);\n if (kept && kept !== data.skin) ops.push(createSetDataOp({ id: child, from: data, to: { ...data, skin: kept }, label }));\n strip(child);\n }\n };\n if (removed.length > 0) strip(id);\n if (deps.applyOps) deps.applyOps(ops, label);\n else scene.applyBatch(ops, label, defaultCommitAdapter(scene, deps.selection?.adapterMethods));\n}\n\nfunction dragAction(env: BoneEnv, editing: () => true | ActionDisabledReason): Action {\n return {\n id: 'bone.drag',\n label: 'Bone — move a joint or draw a bone',\n eligible,\n requires: ['view', 'selection', 'applyOps'],\n enabled: editing,\n invoker: {\n timing: 'ongoing',\n start: ({ deps, drag, world }: InvocationCtx) => {\n const at = env.placed();\n if (!at) return {};\n const press = drag?.start ?? world;\n const before = env.state.active();\n const hit = jointAt(at.skeleton, press.x, press.y, reachOf(env, deps));\n let origin = at.skeleton;\n let name = hit;\n if (name === null) {\n let parent = activeIn(env, origin);\n if (parent === null) ({ skeleton: origin, name: parent } = addJoint(origin, null, press.x, press.y));\n ({ skeleton: origin, name } = addJoint(origin, parent, press.x, press.y));\n }\n const joint = name;\n const from = jointPositions(origin).get(joint)!;\n env.state.setActive(joint);\n env.show(origin);\n return {\n onMove: (move: InvocationCtx) => {\n env.show(moveJoint(origin, joint, from.x + move.world.x - press.x, from.y + move.world.y - press.y));\n },\n // The dispatcher hands `onEnd` a context with empty deps.\n onEnd: (_end: InvocationCtx, reason: 'commit' | 'cancel') => {\n const next = env.shown();\n env.show(null);\n if (reason === 'cancel' || !next) {\n env.state.setActive(before);\n return;\n }\n commitSkeleton(env.scene(), deps, at.id, next, hit === null ? 'Add bone' : 'Move joint');\n },\n };\n },\n },\n };\n}\n\n/** The bone tool's actions: `bone.drag`, `bone.pick`, `bone.remove`, and `bone.clear`. */\nexport function createBoneActions(env: BoneEnv): Action[] {\n const editing = () => (env.placed() ? true : ActionDisabledReason.NotApplicable);\n const hasActive = () => {\n const at = env.placed();\n return at && activeIn(env, at.skeleton) !== null ? true : ActionDisabledReason.NotApplicable;\n };\n return [\n dragAction(env, editing),\n {\n id: 'bone.pick',\n label: 'Bone — pick the active joint',\n eligible,\n requires: ['view'],\n enabled: editing,\n invoker: {\n timing: 'immediate',\n run: (deps, params) => {\n const p = params as { pressX?: number; pressY?: number } | undefined;\n const at = env.placed();\n if (!at || p?.pressX === undefined || p.pressY === undefined) return;\n env.state.setActive(jointAt(at.skeleton, p.pressX, p.pressY, reachOf(env, deps)));\n },\n },\n },\n {\n id: 'bone.remove',\n label: 'Bone — remove the active joint',\n eligible,\n requires: ['selection', 'applyOps'],\n // Declining with no joint active leaves Delete to the ambient `delete`.\n enabled: hasActive,\n invoker: {\n timing: 'immediate',\n run: (deps) => {\n const at = env.placed();\n const name = at && activeIn(env, at.skeleton);\n if (!at || name == null) return;\n const parent = at.skeleton.joints.find((j) => j.name === name)!.parent;\n const { skeleton, removed } = removeJoint(at.skeleton, name);\n commitSkeleton(env.scene(), deps, at.id, skeleton, 'Remove joint', removed);\n env.state.setActive(parent);\n },\n },\n },\n {\n id: 'bone.clear',\n label: 'Bone — clear the active joint',\n eligible,\n enabled: hasActive,\n invoker: { timing: 'immediate', run: () => env.state.setActive(null) },\n },\n ];\n}\n","import { ellipsePath, linePath } from '@weasel-js/geom';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { RenderLayer } from '@weasel-js/core';\nimport { viewToTransform } from '@weasel-js/core';\nimport { worldToScreen } from '@weasel-js/core';\nimport { jointPositions } from '@weasel-js/animator';\nimport type { Skeleton } from '@weasel-js/animator';\n\nconst BONE = '#4f9cf7';\nconst JOINT_FILL = '#ffffff';\n/** A joint dot's radius in screen px, where `targets.boneJointRadiusPx` sets none. */\nexport const JOINT_RADIUS_PX = 4.5;\n\nconst dot = (x: number, y: number, r: number) => ellipsePath({ x: x - r, y: y - r, width: 2 * r, height: 2 * r });\n\n/**\n * A screen-space layer drawing a skeleton as bones and joints at a constant size: a line from\n * each joint to its parent, a ring on each joint, and the active joint filled.\n */\nexport function createBoneOverlay(source: {\n id: string;\n /** The skeleton to draw, in the scene's space, or `null` for none. */\n skeleton(): Skeleton | null;\n active(): string | null;\n subscribe(fn: () => void): () => void;\n /** Joint dot radius in screen px, read each frame. Default `JOINT_RADIUS_PX`. */\n jointRadiusPx?: () => number | undefined;\n}): RenderLayer<unknown> {\n return {\n id: source.id,\n label: 'Bones',\n space: 'screen',\n subscribe: source.subscribe,\n draw: (_data, view) => {\n const skeleton = source.skeleton();\n if (!skeleton || skeleton.joints.length === 0) return [];\n const t = viewToTransform(view);\n const at = new Map<string, { x: number; y: number }>();\n for (const [name, p] of jointPositions(skeleton)) {\n const [x, y] = worldToScreen(p.x, p.y, t);\n at.set(name, { x, y });\n }\n const out: DrawCommand[] = [];\n for (const joint of skeleton.joints) {\n if (joint.parent == null) continue;\n out.push({\n kind: 'path',\n path: linePath(at.get(joint.parent)!, at.get(joint.name)!),\n stroke: { paint: { color: BONE }, width: 2 },\n });\n }\n const active = source.active();\n const r = source.jointRadiusPx?.() ?? JOINT_RADIUS_PX;\n for (const [name, p] of at) {\n out.push({\n kind: 'path',\n path: dot(p.x, p.y, r),\n fill: { paint: { kind: 'solid', color: name === active ? BONE : JOINT_FILL } },\n stroke: { paint: { color: BONE }, width: 1.5 },\n });\n }\n return out;\n },\n };\n}\n","import type { Skin } from '@weasel-js/animator';\nimport { createEmitter } from '@weasel-js/registry';\nimport type { NodeId } from '@weasel-js/core';\nimport type { PaintedSkins } from '@weasel-js/core';\n\n/** What the bone and weight tools share between gestures: the joint they act on, and the\n * weights a stroke has painted and not yet written. */\nexport interface RigEditState extends PaintedSkins {\n /** The active joint's name, or `null`. */\n active(): string | null;\n setActive(joint: string | null): void;\n /** Weights a stroke of the weight tool has painted and not yet written, by node. Empty\n * between strokes. `useRigNode` given this state as `painting` binds them as they change. */\n painted(): ReadonlyMap<NodeId, Skin>;\n /** Replace the painted weights; `null` empties them. */\n paint(next: ReadonlyMap<NodeId, Skin> | null): void;\n /** Notified when the active joint or the painted weights change. Returns an unsubscribe. */\n listen(fn: () => void): () => void;\n}\n\nconst NONE: ReadonlyMap<NodeId, Skin> = new Map();\n\n/** A `RigEditState` with no joint active. Hand one to both tools so a joint picked with the\n * bone tool is the one the weight tool paints. */\nexport function createRigEditState(): RigEditState {\n let active: string | null = null;\n let painted = NONE;\n const changed = createEmitter();\n return {\n active: () => active,\n setActive(joint) {\n if (joint === active) return;\n active = joint;\n changed.emit();\n },\n painted: () => painted,\n paint(next) {\n if ((next ?? NONE) === painted) return;\n painted = next ?? NONE;\n changed.emit();\n },\n listen: changed.subscribe,\n };\n}\n","import { useMemo, useRef } from 'react';\nimport { useKitSettings, useLatest } from '@weasel-js/react';\nimport type { ToolKeybinding } from '@weasel-js/routing';\nimport type { NodeId, Scene } from '@weasel-js/core';\nimport type { Skeleton } from '@weasel-js/animator';\nimport { defineTool } from '@weasel-js/core';\nimport type { Tool } from '@weasel-js/core';\nimport { createBoneActions, type BoneEnv } from './boneActions';\nimport { createBoneOverlay } from './boneOverlay';\nimport { placedRig } from './placedRig';\nimport { createRigEditState, type RigEditState } from './rigEditState';\nimport { createEmitter } from '@weasel-js/registry';\nimport { icon } from '../icons.gen';\n\n/** Options for `useBoneTool`. */\nexport interface UseBoneToolOptions {\n scene: Scene<unknown, string, unknown>;\n /** The node whose data holds the skeleton, or will once a bone is drawn: usually read from\n * the selection. `null` leaves the tool with nothing to edit. */\n rig: () => NodeId | null;\n /** Shared with `useWeightTool`, so both act on one joint. Default: the tool's own. */\n state?: RigEditState;\n /** Override the default keybinding (`{ key: 'J' }`). Pass `null` to omit. */\n keybinding?: ToolKeybinding | null;\n /** Screen px around a joint within which a press takes it. Default 10. */\n reach?: number;\n}\n\n/**\n * Draws and edits the skeleton in a node's `data.skeleton`. A drag on a joint moves it, with\n * the joints under it; a drag from anywhere else draws a bone, from the active joint if there\n * is one and from the press if not, and the new joint becomes the active one. A click picks\n * the active joint or clears it, Delete removes it with everything under it, taking its\n * weights out of every skin beneath the rig node, and Escape clears it. Each edit is one undo\n * entry.\n */\nexport function useBoneTool(options: UseBoneToolOptions): Tool {\n const ownState = useMemo(() => createRigEditState(), []);\n const state = options.state ?? ownState;\n const opts = useLatest({ scene: options.scene, rig: options.rig, reach: options.reach ?? 10 });\n /** The skeleton a drag is shaping, in the scene's space. */\n const liveRef = useRef<Skeleton | null>(null);\n\n const env = useMemo(() => {\n const changed = createEmitter();\n const tool: BoneEnv & { listen(fn: () => void): () => void } = {\n state,\n scene: () => opts.current.scene,\n reachPx: () => opts.current.reach,\n placed: () => placedRig(opts.current.scene, opts.current.rig()),\n shown: () => liveRef.current,\n show(skeleton) {\n liveRef.current = skeleton;\n changed.emit();\n },\n listen(fn) {\n const offOwn = changed.subscribe(fn);\n const off = state.listen(fn);\n return () => { offOwn(); off(); };\n },\n };\n return tool;\n }, [opts, state]);\n\n const actions = useMemo(() => createBoneActions(env), [env]);\n const kitSettings = useKitSettings();\n const overlay = useMemo(() => createBoneOverlay({\n id: 'bone-tool-skeleton',\n skeleton: () => env.shown() ?? env.placed()?.skeleton ?? null,\n active: state.active,\n subscribe: env.listen,\n jointRadiusPx: () => kitSettings.get('targets.boneJointRadiusPx'),\n }), [env, state, kitSettings]);\n\n const { keybinding } = options;\n return useMemo(() => defineTool({\n id: 'bone',\n capabilities: ['edits-page'],\n hookName: 'useBoneTool',\n cursor: 'crosshair',\n presentation: { label: 'Bone', icon: /* @icon */ 'rig:bone', group: 'shape' },\n ...(keybinding === null ? {} : { keybinding: keybinding ?? { key: 'J' } }),\n actions,\n overlay,\n onDeactivate: () => env.show(null),\n bindings: [\n { spec: { kind: 'click', mods: { shift: 'optional', alt: 'optional' } }, actionId: 'bone.pick' },\n { spec: { kind: 'drag', mods: { shift: 'optional', alt: 'optional' } }, actionId: 'bone.drag' },\n { spec: { kind: 'key', key: ['Delete', 'Backspace'] }, actionId: 'bone.remove' },\n { spec: { kind: 'key', key: 'Escape' }, actionId: 'bone.clear' },\n ],\n }), [actions, overlay, keybinding, env]);\n}\n","import type { Action, ActionDeps, InvocationCtx } from '@weasel-js/routing';\nimport { ActionDisabledReason } from '@weasel-js/routing';\nimport type { PolygonPath } from '@weasel-js/geom';\nimport type { Op } from '@weasel-js/history';\nimport { createSetDataOp } from '@weasel-js/core';\nimport type { NodeId, Scene } from '@weasel-js/core';\nimport { defaultCommitAdapter } from '@weasel-js/core';\nimport type { RigNodeData } from '@weasel-js/core';\nimport { jointAt } from '@weasel-js/animator';\nimport { skinnable, type Skin } from '@weasel-js/animator';\nimport { dataRestPath } from '@weasel-js/core';\nimport { brushWeights, setWeights } from '@weasel-js/animator';\nimport { sceneReach, type PlacedRig } from '../bone/placedRig';\nimport type { RigEditState } from '../bone/rigEditState';\n\n/** One path under the rig node, where it is drawn, with the weights now showing. */\nexport interface WeighedPath {\n id: NodeId;\n path: PolygonPath;\n skin: Skin;\n}\n\n/** What the weight tool's actions read and write, all of it the tool's own. */\nexport interface WeightEnv {\n scene(): Scene<unknown, string, unknown>;\n placed(): PlacedRig | null;\n state: RigEditState;\n /** Screen px around a joint within which a click picks it. */\n reachPx(): number;\n /** The brush's radius in screen px. */\n radiusPx(): number;\n /** Weights a stroke has painted and not yet committed, by node. */\n painted(): ReadonlyMap<NodeId, Skin>;\n paint(next: ReadonlyMap<NodeId, Skin> | null): void;\n}\n\n/** Every path under the rig node, with what `env.painted()` holds for it in place of its\n * `data.skin`. */\nexport function weighedPaths(env: Pick<WeightEnv, 'scene' | 'placed' | 'painted'>): WeighedPath[] {\n const rig = env.placed();\n if (!rig) return [];\n const scene = env.scene();\n const painted = env.painted();\n const out: WeighedPath[] = [];\n const walk = (parent: NodeId): void => {\n for (const id of scene.childrenOf(parent)) {\n const node = scene.get(id);\n const rest = node && dataRestPath(node);\n if (node && rest) {\n const skin = painted.get(id) ?? (node.data as RigNodeData | null | undefined)?.skin ?? {};\n out.push({ id, path: skinnable(rest), skin });\n }\n walk(id);\n }\n };\n walk(rig.id);\n return out;\n}\n\nconst eligible = { capability: 'edits-page' } as const;\n\nconst activeJoint = (env: WeightEnv): string | null => {\n const name = env.state.active();\n return name !== null && env.placed()?.skeleton.joints.some((j) => j.name === name) ? name : null;\n};\n\n/** One dab of the brush at `(x, y)`: what `env.painted()` becomes, or `null` if no point moved. */\nfunction dab(env: WeightEnv, deps: ActionDeps, joint: string, x: number, y: number, mode: 'add' | 'remove'): Map<NodeId, Skin> | null {\n const radius = sceneReach(deps, env.radiusPx());\n let next: Map<NodeId, Skin> | null = null;\n for (const { id, path, skin } of weighedPaths(env)) {\n const hits = brushWeights(path.coords, x, y, radius, skin[joint], mode);\n if (hits.size === 0) continue;\n next ??= new Map(env.painted());\n next.set(id, setWeights(skin, path.coords.length >> 1, joint, hits));\n }\n return next;\n}\n\n/** Write what a stroke painted into each node's `data.skin`, as one undo entry. */\nfunction commitPainted(env: WeightEnv, deps: ActionDeps): void {\n const scene = env.scene();\n const ops: Op[] = [];\n for (const [id, skin] of env.painted()) {\n const data = scene.get(id)?.data;\n if (data !== undefined) ops.push(createSetDataOp({ id, from: data, to: { ...(data as object), skin }, label: 'Paint weights' }));\n }\n if (ops.length > 0) {\n if (deps.applyOps) deps.applyOps(ops, 'Paint weights');\n else scene.applyBatch(ops, 'Paint weights', defaultCommitAdapter(scene, deps.selection?.adapterMethods));\n }\n env.paint(null);\n}\n\n/** The weight tool's actions: `weight.paint` and `weight.click`. */\nexport function createWeightActions(env: WeightEnv): Action[] {\n const editing = () => (env.placed() ? true : ActionDisabledReason.NotApplicable);\n return [\n {\n id: 'weight.paint',\n label: 'Weight — paint the active joint\\'s weights',\n eligible,\n requires: ['view', 'selection', 'applyOps'],\n enabled: () => (activeJoint(env) !== null ? true : ActionDisabledReason.NotApplicable),\n invoker: {\n timing: 'ongoing',\n start: ({ deps, drag, world, modifiers }: InvocationCtx) => {\n const joint = activeJoint(env);\n if (joint === null) return {};\n const stroke = (x: number, y: number, alt: boolean): void => {\n const next = dab(env, deps, joint, x, y, alt ? 'remove' : 'add');\n if (next) env.paint(next);\n };\n const press = drag?.start ?? world;\n stroke(press.x, press.y, modifiers.alt);\n return {\n onMove: (move: InvocationCtx) => stroke(move.world.x, move.world.y, move.modifiers.alt),\n // The dispatcher hands `onEnd` a context with empty deps.\n onEnd: (_end: InvocationCtx, reason: 'commit' | 'cancel') => {\n if (reason === 'cancel') env.paint(null);\n else commitPainted(env, deps);\n },\n };\n },\n },\n },\n {\n id: 'weight.click',\n label: 'Weight — pick a joint, or dab its weight',\n eligible,\n requires: ['view', 'selection', 'applyOps'],\n enabled: editing,\n invoker: {\n timing: 'immediate',\n run: (deps, params) => {\n const p = params as { pressX?: number; pressY?: number; mods?: { alt?: boolean } } | undefined;\n const rig = env.placed();\n if (!rig || p?.pressX === undefined || p.pressY === undefined) return;\n const hit = jointAt(rig.skeleton, p.pressX, p.pressY, sceneReach(deps, env.reachPx()));\n if (hit !== null) {\n env.state.setActive(hit);\n return;\n }\n const joint = activeJoint(env);\n const next = joint === null ? null : dab(env, deps, joint, p.pressX, p.pressY, p.mods?.alt ? 'remove' : 'add');\n if (!next) return;\n env.paint(next);\n commitPainted(env, deps);\n },\n },\n },\n ];\n}\n","import { ellipsePath } from '@weasel-js/geom';\nimport type { DrawCommand } from '@weasel-js/renderer';\nimport type { RenderLayer } from '@weasel-js/core';\nimport { viewToTransform } from '@weasel-js/core';\nimport { worldToScreen } from '@weasel-js/core';\nimport type { WeighedPath } from './weightActions';\n\n/** A weighed point's dot radius in screen px, where `targets.weightPointRadiusPx` sets none. */\nexport const POINT_RADIUS_PX = 3.5;\nconst RIM = '#10161d';\n/** No weight, and all of it. */\nconst COLD = [0x1f, 0x6f, 0x8b];\nconst HOT = [0xff, 0x8a, 0x3d];\nconst IDLE = '#8a94a3';\n\nconst mix = (w: number): string => {\n const c = COLD.map((v, k) => Math.round(v + (HOT[k] - v) * w));\n return `rgb(${c[0]}, ${c[1]}, ${c[2]})`;\n};\n\n/**\n * A screen-space layer drawing a dot on every point of every path under a rig, anchors and\n * curve handles alike, colored by how much the active joint weighs it. With no joint active\n * the dots are gray.\n */\nexport function createWeightOverlay(source: {\n id: string;\n paths(): readonly WeighedPath[];\n active(): string | null;\n subscribe(fn: () => void): () => void;\n /** Point dot radius in screen px, read each frame. Default `POINT_RADIUS_PX`. */\n pointRadiusPx?: () => number | undefined;\n}): RenderLayer<unknown> {\n return {\n id: source.id,\n label: 'Weights',\n space: 'screen',\n subscribe: source.subscribe,\n draw: (_data, view) => {\n const t = viewToTransform(view);\n const active = source.active();\n const r = source.pointRadiusPx?.() ?? POINT_RADIUS_PX;\n const out: DrawCommand[] = [];\n for (const { path, skin } of source.paths()) {\n const weights = active === null ? undefined : skin[active];\n let total: number[] | undefined;\n if (active !== null) {\n total = new Array<number>(path.coords.length >> 1).fill(0);\n for (const joint in skin) skin[joint].forEach((w, i) => { if (i < total!.length) total![i] += w; });\n }\n for (let i = 0; 2 * i + 1 < path.coords.length; i++) {\n const [x, y] = worldToScreen(path.coords[2 * i], path.coords[2 * i + 1], t);\n const share = total && total[i] > 0 ? (weights?.[i] ?? 0) / total[i] : 0;\n out.push({\n kind: 'path',\n path: ellipsePath({ x: x - r, y: y - r, width: 2 * r, height: 2 * r }),\n fill: { paint: { kind: 'solid', color: active === null ? IDLE : mix(share) } },\n stroke: { paint: { color: RIM }, width: 1 },\n });\n }\n }\n return out;\n },\n };\n}\n","import { useMemo } from 'react';\nimport { useKitSettings, useLatest } from '@weasel-js/react';\nimport type { ToolKeybinding } from '@weasel-js/routing';\nimport type { NodeId, Scene } from '@weasel-js/core';\nimport { defineTool } from '@weasel-js/core';\nimport type { Tool } from '@weasel-js/core';\nimport { createBoneOverlay } from '../bone/boneOverlay';\nimport { placedRig } from '../bone/placedRig';\nimport { createRigEditState, type RigEditState } from '../bone/rigEditState';\nimport { createWeightActions, weighedPaths, type WeightEnv } from './weightActions';\nimport { createWeightOverlay } from './weightOverlay';\nimport { icon } from '../icons.gen';\n\n/** Options for `useWeightTool`. */\nexport interface UseWeightToolOptions {\n scene: Scene<unknown, string, unknown>;\n /** The node whose data holds the skeleton; the tool paints the paths under it. */\n rig: () => NodeId | null;\n /** Shared with `useBoneTool`, so both act on one joint. Default: the tool's own. */\n state?: RigEditState;\n /** Override the default keybinding (`{ key: 'W' }`). Pass `null` to omit. */\n keybinding?: ToolKeybinding | null;\n /** The brush's radius in screen px. Default 24. */\n radius?: number;\n /** Screen px around a joint within which a click picks it. Default 10. */\n reach?: number;\n}\n\n/**\n * Paints how much the active joint weighs each point of the paths under a rig node, into\n * their `data.skin`. A click on a joint makes it the active one. A drag is a round brush:\n * points under it follow the active joint more, the nearer the brush's center, and their\n * other joints give way; with Alt held they follow it less. A click away from any joint is\n * one dab. Every point shows as a dot colored by the active joint's share of it. A stroke is\n * one undo entry, and until it ends its weights are in `state.painted()`, which `useRigNode`\n * binds when given the same `state` as `painting`.\n */\nexport function useWeightTool(options: UseWeightToolOptions): Tool {\n const ownState = useMemo(() => createRigEditState(), []);\n const state = options.state ?? ownState;\n const opts = useLatest({\n scene: options.scene, rig: options.rig, radius: options.radius ?? 24, reach: options.reach ?? 10,\n });\n const env = useMemo((): WeightEnv => ({\n state,\n scene: () => opts.current.scene,\n placed: () => placedRig(opts.current.scene, opts.current.rig()),\n reachPx: () => opts.current.reach,\n radiusPx: () => opts.current.radius,\n painted: state.painted,\n paint: state.paint,\n }), [opts, state]);\n\n const actions = useMemo(() => createWeightActions(env), [env]);\n const kitSettings = useKitSettings();\n const overlay = useMemo(() => [\n createWeightOverlay({\n id: 'weight-tool-points',\n paths: () => weighedPaths(env),\n active: state.active,\n subscribe: state.listen,\n pointRadiusPx: () => kitSettings.get('targets.weightPointRadiusPx'),\n }),\n createBoneOverlay({\n id: 'weight-tool-skeleton',\n skeleton: () => env.placed()?.skeleton ?? null,\n active: state.active,\n subscribe: state.listen,\n jointRadiusPx: () => kitSettings.get('targets.boneJointRadiusPx'),\n }),\n ], [env, state, kitSettings]);\n\n const { keybinding } = options;\n return useMemo(() => defineTool({\n id: 'weight',\n capabilities: ['edits-page'],\n hookName: 'useWeightTool',\n cursor: 'crosshair',\n presentation: { label: 'Weight', icon: /* @icon */ 'rig:weight', group: 'shape' },\n ...(keybinding === null ? {} : { keybinding: keybinding ?? { key: 'W' } }),\n actions,\n overlay,\n onDeactivate: () => env.paint(null),\n bindings: [\n { spec: { kind: 'click', mods: { shift: 'optional', alt: 'optional' } }, actionId: 'weight.click' },\n { spec: { kind: 'drag', mods: { shift: 'optional', alt: 'optional' } }, actionId: 'weight.paint' },\n ],\n }), [actions, overlay, keybinding, env]);\n}\n","import { useMemo } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport type { NodeId, RigNodeData, Scene, Tool } from '@weasel-js/core';\nimport { useBoneTool } from './bone/useBoneTool';\nimport { createRigEditState, type RigEditState } from './bone/rigEditState';\nimport { useWeightTool } from './weight/useWeightTool';\nimport type { RigToolId } from './toolSets';\n\n/**\n * The rig a selection names: with one node selected, the nearest of it and its\n * ancestors whose `data.skeleton` exists, else that node itself, so a first\n * bone can be drawn onto it. With none or several selected, `null`.\n */\nexport function rigOfSelection(\n scene: { get(id: NodeId): { parent: NodeId | null; data?: unknown } | undefined },\n ids: readonly NodeId[],\n): NodeId | null {\n if (ids.length !== 1) return null;\n const own = ids[0];\n for (let id: NodeId | null = own; id !== null;) {\n const node = scene.get(id);\n if (!node) break;\n if ((node.data as RigNodeData | null | undefined)?.skeleton) return id;\n id = node.parent;\n }\n return scene.get(own) ? own : null;\n}\n\n/** Options for {@link useRigTools}. */\nexport interface UseRigToolsOptions {\n /** The scene the tools edit: the one given to `<SceneCanvas>`. */\n scene: Scene<unknown, string, unknown>;\n /** The selection that names the rig, read when a tool acts. Default the scene's own,\n * which is the canvas's unless `<SceneCanvas selection>` supplies another. */\n selection?: () => readonly NodeId[];\n /** Fixes the rig node, in place of the one the selection names. */\n rig?: () => NodeId | null;\n /** The joint both tools act on and the stroke being painted. Hand the same one to\n * `useRigNode` as `painting` so a stroke shows as it is painted. Default the hook's own. */\n state?: RigEditState;\n /** Screen px around a joint within which a press takes it. */\n reach?: number;\n /** The weight tool's brush radius, in screen px. */\n radius?: number;\n}\n\n/**\n * Every tool in {@link RIG_TOOLS}, keyed by id, for `<SceneCanvas tools>`: the bone and\n * weight tools on one scene, sharing one `RigEditState`, and acting on the rig the\n * selection names unless `rig` fixes one.\n */\nexport function useRigTools(options: UseRigToolsOptions): Record<RigToolId, Tool> {\n const { scene } = options;\n const latest = useLatest({ scene, selection: options.selection, rig: options.rig });\n const rig = useMemo(() => (): NodeId | null => {\n const { scene: s, selection, rig: fixed } = latest.current;\n return fixed ? fixed() : rigOfSelection(s, selection ? selection() : s.getSelection());\n }, [latest]);\n const ownState = useMemo(() => createRigEditState(), []);\n const state = options.state ?? ownState;\n const { reach, radius } = options;\n const bone = useBoneTool({ scene, rig, state, reach });\n const weight = useWeightTool({ scene, rig, state, reach, radius });\n return useMemo(() => ({ bone, weight }), [bone, weight]);\n}\n","import type {} from '@weasel-js/react';\nimport { pref } from '@weasel-js/prefs';\nimport { JOINT_RADIUS_PX } from './bone/boneOverlay';\nimport { POINT_RADIUS_PX } from './weight/weightOverlay';\n\n/** The rig tools' `targets` leaves: overlay dot sizes, in CSS px. */\nexport interface RigKitTargetSettings {\n boneJointRadiusPx: number;\n weightPointRadiusPx: number;\n}\n\ndeclare module '@weasel-js/react' {\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n interface KitTargetSettings extends RigKitTargetSettings {}\n}\n\nconst pxLeaf = (name: string, def: number) =>\n pref.number(name, def, { unit: { scale: 1, suffix: 'px' }, min: 1, max: 16, step: 0.5 });\n\n/** The rig tools' `targets` leaves as a prefs group whose paths are the `KitSettings` paths. */\nexport const rigKitSettingsSchema = pref.group('Rig', {\n targets: pref.group('Targets', {\n boneJointRadiusPx: pxLeaf('Bone joint dot', JOINT_RADIUS_PX),\n weightPointRadiusPx: pxLeaf('Weight point dot', POINT_RADIUS_PX),\n }),\n});\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/rig",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.11.1",
|
|
4
|
+
"description": "Rigging tools for a weasel canvas: the bone tool, which draws and edits a node's skeleton, and the weight tool, which paints how much each joint moves the paths under it.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./icons": {
|
|
17
|
+
"import": "./dist/icons.table.js",
|
|
18
|
+
"types": "./dist/icons.table.d.ts"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
21
|
+
},
|
|
22
|
+
"weaselIcons": {
|
|
23
|
+
"table": "./dist/icons.table.json"
|
|
24
|
+
},
|
|
25
|
+
"author": "orochi235",
|
|
26
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
30
|
+
"directory": "packages/rig"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup && cp src/icons.table.json dist/"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@weasel-js/animator": "1.11.1",
|
|
48
|
+
"@weasel-js/history": "1.11.1",
|
|
49
|
+
"@weasel-js/icons": "1.11.1",
|
|
50
|
+
"@weasel-js/prefs": "1.11.1",
|
|
51
|
+
"@weasel-js/react": "1.11.1",
|
|
52
|
+
"@weasel-js/registry": "1.11.1",
|
|
53
|
+
"@weasel-js/renderer": "1.11.1",
|
|
54
|
+
"@weasel-js/routing": "1.11.1"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@weasel-js/core": "1.11.1",
|
|
58
|
+
"@weasel-js/geom": "1.11.1",
|
|
59
|
+
"react": ">=18"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public",
|
|
63
|
+
"provenance": true
|
|
64
|
+
}
|
|
65
|
+
}
|