@weasel-js/symmetry 0.0.0-stage → 1.10.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 +62 -2
- package/dist/index.d.ts +271 -0
- package/dist/index.js +447 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -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,63 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @weasel-js/symmetry
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Live symmetry groups. A symmetry group is a container whose mirrored, rotated
|
|
4
|
+
and repeated copies are drawn from its real children rather than stored, so
|
|
5
|
+
editing a child reaches every copy, and a click on a copy selects the child it
|
|
6
|
+
copies.
|
|
7
|
+
|
|
8
|
+
Part of [weasel](https://github.com/orochi235/weasel), a domain-agnostic 2D
|
|
9
|
+
scene-graph canvas kit for React. See the
|
|
10
|
+
[API reference](https://orochi235.github.io/weasel/#pkg/symmetry).
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @weasel-js/symmetry
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Kinds
|
|
17
|
+
|
|
18
|
+
Each kind is a list of transforms about the group's center, the original not
|
|
19
|
+
among them. `angle` is in radians from +x toward +y, which is clockwise on
|
|
20
|
+
screen. `VERTICAL_AXIS` mirrors left and right, and `HORIZONTAL_AXIS` mirrors
|
|
21
|
+
top and bottom.
|
|
22
|
+
|
|
23
|
+
| Kind | Copies | Fields read |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `mirror` | one reflection across the line at `angle` | `angle` |
|
|
26
|
+
| `both` | reflections across `angle` and the line square to it, and the half turn | `angle` |
|
|
27
|
+
| `point` | the half turn | — |
|
|
28
|
+
| `radial` | `count - 1` turns, evenly spaced | `count` |
|
|
29
|
+
| `kaleidoscope` | the radial turns, and a mirror through each wedge | `count`, `angle` |
|
|
30
|
+
| `repeat` | `count - 1` steps of `spacing` along `angle`, every other one mirrored under `glide` | `count`, `spacing`, `angle`, `glide` |
|
|
31
|
+
|
|
32
|
+
`SYMMETRY_KINDS` holds them as a table, and a new kind is a row in it.
|
|
33
|
+
|
|
34
|
+
## Use
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
const scene = createScene({ systemLayers: [{ id: 'main' }], registry: symmetryRegistry });
|
|
38
|
+
const symmetry = createSymmetry(scene);
|
|
39
|
+
symmetry.create({ center: { x: 300, y: 200 }, symmetry: { kind: 'kaleidoscope', count: 6 } });
|
|
40
|
+
|
|
41
|
+
<SceneCanvas scene={scene} ambient={[symmetry.contribution]} />
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`symmetry.contribution` does three things on the canvas:
|
|
45
|
+
|
|
46
|
+
- It draws the active group's guide: the mirror lines, and the handles that
|
|
47
|
+
move the center, turn the axis, and stretch a repeat. Shift holds the angle
|
|
48
|
+
to 15° steps.
|
|
49
|
+
- It answers core's `insertInto` dep, so every tool draws into the active
|
|
50
|
+
group.
|
|
51
|
+
- It answers core's `pathSymmetry` dep. When a path's anchors are dragged or
|
|
52
|
+
nudged, if the path is its own image under some of its group's transforms,
|
|
53
|
+
every image of what moves follows, and an anchor on an axis stays on it.
|
|
54
|
+
|
|
55
|
+
`symmetry.update(id, { spec, center })` changes a group as one undo step.
|
|
56
|
+
`symmetry.bake(id)` replaces it with a plain group holding real copies of its
|
|
57
|
+
path leaves.
|
|
58
|
+
|
|
59
|
+
Without `symmetryRegistry` a group still draws, but `scene.toJSON()` refuses
|
|
60
|
+
it. Exported to SVG, each copy is written as a transformed group.
|
|
61
|
+
|
|
62
|
+
Under the hood the group is a core container with `copiesFromPose`, which any
|
|
63
|
+
container can carry.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { Affine } from '@weasel-js/geom';
|
|
2
|
+
import { NodeId, Scene, CopiesFromPoseFn, AddNodeSpec, SceneRegistry, View, RenderLayer, SurfaceContribution, Action } from '@weasel-js/core';
|
|
3
|
+
|
|
4
|
+
/** Which copies a symmetry group draws. */
|
|
5
|
+
type SymmetryKind = 'mirror' | 'both' | 'point' | 'radial' | 'kaleidoscope' | 'repeat';
|
|
6
|
+
/**
|
|
7
|
+
* Everything a symmetry group's copies are built from, besides its center.
|
|
8
|
+
* Each kind reads only the fields it needs, so switching kind keeps the rest.
|
|
9
|
+
*/
|
|
10
|
+
interface SymmetrySpec {
|
|
11
|
+
kind: SymmetryKind;
|
|
12
|
+
/**
|
|
13
|
+
* Radians, measured from +x toward +y, which is clockwise on screen. The
|
|
14
|
+
* axis a mirror reflects across, the first of `both`'s two axes and of a
|
|
15
|
+
* kaleidoscope's mirrors, and the direction a repeat steps in.
|
|
16
|
+
* {@link VERTICAL_AXIS} mirrors left and right, {@link HORIZONTAL_AXIS}
|
|
17
|
+
* top and bottom.
|
|
18
|
+
*/
|
|
19
|
+
angle: number;
|
|
20
|
+
/** How many there are, the original included: wedges round the center for
|
|
21
|
+
* `radial` and `kaleidoscope`, places along the line for `repeat`. */
|
|
22
|
+
count: number;
|
|
23
|
+
/** World distance from one repeat to the next. */
|
|
24
|
+
spacing: number;
|
|
25
|
+
/** `repeat` only: every other repeat is mirrored across the line it steps
|
|
26
|
+
* along, a glide reflection. */
|
|
27
|
+
glide: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** The angle of an axis running up and down the screen. */
|
|
30
|
+
declare const VERTICAL_AXIS: number;
|
|
31
|
+
/** The angle of an axis running across the screen. */
|
|
32
|
+
declare const HORIZONTAL_AXIS = 0;
|
|
33
|
+
/** A mirror across the vertical axis, with the other fields at their defaults. */
|
|
34
|
+
declare const DEFAULT_SYMMETRY: SymmetrySpec;
|
|
35
|
+
type Pt$4 = {
|
|
36
|
+
x: number;
|
|
37
|
+
y: number;
|
|
38
|
+
};
|
|
39
|
+
/** The reflection across the line through `c` at `angle`. */
|
|
40
|
+
declare function mirrorAcross(c: Pt$4, angle: number): Affine;
|
|
41
|
+
/** The rotation by `angle` about `c`. */
|
|
42
|
+
declare function turnAbout(c: Pt$4, angle: number): Affine;
|
|
43
|
+
/** One kind: how its copies are built, and whether they form a finite group —
|
|
44
|
+
* one a path can be its own image under, which a repeat's never can. */
|
|
45
|
+
interface KindDef {
|
|
46
|
+
readonly label: string;
|
|
47
|
+
readonly group: boolean;
|
|
48
|
+
transforms(spec: SymmetrySpec, c: Pt$4): Affine[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Every kind, by name. A new kind, a wallpaper tiling say, is a row here: the
|
|
52
|
+
* group, the guide layer and the controls all read the table.
|
|
53
|
+
*/
|
|
54
|
+
declare const SYMMETRY_KINDS: Readonly<Record<SymmetryKind, KindDef>>;
|
|
55
|
+
/**
|
|
56
|
+
* The copies `spec` draws about `center`, as world affines, the original not
|
|
57
|
+
* among them. For a kind that forms a group this is the whole group less its
|
|
58
|
+
* identity.
|
|
59
|
+
*/
|
|
60
|
+
declare function symmetryTransforms(spec: SymmetrySpec, center: Pt$4): Affine[];
|
|
61
|
+
|
|
62
|
+
/** What a symmetry group keeps in its `data`. Its center is its pose. */
|
|
63
|
+
interface SymmetryGroupData {
|
|
64
|
+
symmetry: SymmetrySpec;
|
|
65
|
+
}
|
|
66
|
+
/** The box a symmetry group is posed at: empty, at its center. */
|
|
67
|
+
interface SymmetryPose {
|
|
68
|
+
x: number;
|
|
69
|
+
y: number;
|
|
70
|
+
width: number;
|
|
71
|
+
height: number;
|
|
72
|
+
}
|
|
73
|
+
type Pt$3 = {
|
|
74
|
+
x: number;
|
|
75
|
+
y: number;
|
|
76
|
+
};
|
|
77
|
+
/** Where a group's copies turn and reflect about: the middle of its pose. */
|
|
78
|
+
declare function centerOfPose(pose: unknown): Pt$3;
|
|
79
|
+
/** The pose that puts a group's center at `c`. */
|
|
80
|
+
declare function poseAtCenter(c: Pt$3): SymmetryPose;
|
|
81
|
+
/** The spec a group's `data` holds, with any field it lacks at its default. */
|
|
82
|
+
declare function specOfData(data: unknown): SymmetrySpec;
|
|
83
|
+
/** A symmetry group's copies: the `copiesFromPose` every group carries. */
|
|
84
|
+
declare const symmetryCopies: CopiesFromPoseFn<unknown>;
|
|
85
|
+
/** The registry key a group's `copiesFromPose` is saved under. */
|
|
86
|
+
declare const SYMMETRY_COPIES_KEY = "weasel-symmetry:copies";
|
|
87
|
+
/** The registry key a group's `clipFromPose`, which clips nothing, is saved under. */
|
|
88
|
+
declare const SYMMETRY_CLIP_KEY = "weasel-symmetry:no-clip";
|
|
89
|
+
/**
|
|
90
|
+
* The entries a scene needs to save and restore symmetry groups: pass it to
|
|
91
|
+
* `createScene({ registry })`, merged with any of your own. Without it a
|
|
92
|
+
* group still draws its copies, but `toJSON` refuses the scene.
|
|
93
|
+
*/
|
|
94
|
+
declare const symmetryRegistry: SceneRegistry<unknown>;
|
|
95
|
+
/** What {@link symmetryGroupSpec} builds a group from. */
|
|
96
|
+
interface SymmetryGroupOptions<TLayer extends string = string> {
|
|
97
|
+
layer: TLayer;
|
|
98
|
+
center: Pt$3;
|
|
99
|
+
symmetry?: Partial<SymmetrySpec>;
|
|
100
|
+
id?: NodeId;
|
|
101
|
+
parent?: NodeId | null;
|
|
102
|
+
index?: number;
|
|
103
|
+
}
|
|
104
|
+
/** The `scene.add` spec for a new, empty symmetry group. */
|
|
105
|
+
declare function symmetryGroupSpec<TLayer extends string>(opts: SymmetryGroupOptions<TLayer>): AddNodeSpec<SymmetryGroupData, TLayer, SymmetryPose>;
|
|
106
|
+
type AnyScene$2 = Scene<unknown, string, unknown>;
|
|
107
|
+
/** Whether `id` names a symmetry group in `scene`. */
|
|
108
|
+
declare function isSymmetryGroup(scene: AnyScene$2, id: NodeId | string | null | undefined): boolean;
|
|
109
|
+
/** The nearest symmetry group holding `id`, or `id` itself when it is one. */
|
|
110
|
+
declare function symmetryGroupOf(scene: AnyScene$2, id: NodeId | string): NodeId | null;
|
|
111
|
+
/** The spec and center of group `id`, or `null` when it is not one. */
|
|
112
|
+
declare function readGroup(scene: AnyScene$2, id: NodeId | string | null): {
|
|
113
|
+
spec: SymmetrySpec;
|
|
114
|
+
center: Pt$3;
|
|
115
|
+
} | null;
|
|
116
|
+
/**
|
|
117
|
+
* Every transform a path inside group `id` should stay its own image under
|
|
118
|
+
* while it is edited, or none for a kind that is not a group. In world
|
|
119
|
+
* coordinates, for the `pathSymmetry` dep.
|
|
120
|
+
*/
|
|
121
|
+
declare function editSymmetries(scene: AnyScene$2, id: NodeId | string | null): Affine[];
|
|
122
|
+
|
|
123
|
+
type AnyScene$1 = Scene<unknown, string, unknown>;
|
|
124
|
+
/** What {@link bakeSymmetry} did. */
|
|
125
|
+
interface BakeResult {
|
|
126
|
+
/** The plain group that replaced the symmetry group. */
|
|
127
|
+
group: NodeId;
|
|
128
|
+
/** Leaves drawn by something other than a path in their data — text, an
|
|
129
|
+
* image — which kept their place but whose copies were not made. */
|
|
130
|
+
skipped: NodeId[];
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Turn symmetry group `id` into a plain group holding its children and, for
|
|
134
|
+
* every copy it drew, a real node: one undo step. A symmetry group nested
|
|
135
|
+
* inside is baked first. Copies are made of path leaves, their outline
|
|
136
|
+
* carried through the copy's transform into a pose of its own; a leaf drawn
|
|
137
|
+
* any other way is reported in `skipped`. Returns `null` when `id` is not a
|
|
138
|
+
* symmetry group.
|
|
139
|
+
*
|
|
140
|
+
* Written for a scene of absolute poses, the kit's default, and for the
|
|
141
|
+
* kit's path data, `{ path, fill?, stroke? }`. A paint that is laid out in
|
|
142
|
+
* the shape's own box, a gradient say, is copied as it is.
|
|
143
|
+
*/
|
|
144
|
+
declare function bakeSymmetry(scene: AnyScene$1, id: NodeId, label?: string): BakeResult | null;
|
|
145
|
+
|
|
146
|
+
/** The guide layer's id. */
|
|
147
|
+
declare const SYMMETRY_GUIDE_ID = "weasel-symmetry-guide";
|
|
148
|
+
/** The affordance kind a press on one of the guide's handles arrives as. */
|
|
149
|
+
declare const SYMMETRY_HANDLE_KIND = "layer:weasel-symmetry-guide";
|
|
150
|
+
/** Which handle a press took: the center, the knob that turns the axis, or
|
|
151
|
+
* the end of a repeat's first step. */
|
|
152
|
+
type SymmetryHandle = 'center' | 'angle' | 'step';
|
|
153
|
+
/** What a press on a handle carries to the drag action. */
|
|
154
|
+
interface SymmetryHandlePress {
|
|
155
|
+
handle: SymmetryHandle;
|
|
156
|
+
group: string;
|
|
157
|
+
}
|
|
158
|
+
/** How the guide looks. Sizes are screen pixels. */
|
|
159
|
+
interface SymmetryGuideStyle {
|
|
160
|
+
color?: string;
|
|
161
|
+
/** Spokes and repeat ticks, which say where copies go rather than what reflects. */
|
|
162
|
+
faintColor?: string;
|
|
163
|
+
handleRadiusPx?: number;
|
|
164
|
+
/** How far from a handle a press still takes it. */
|
|
165
|
+
hitRadiusPx?: number;
|
|
166
|
+
/** How far from the center the angle knob sits. */
|
|
167
|
+
knobDistancePx?: number;
|
|
168
|
+
}
|
|
169
|
+
type Pt$2 = {
|
|
170
|
+
x: number;
|
|
171
|
+
y: number;
|
|
172
|
+
};
|
|
173
|
+
/** What the guide draws: the active group's spec and center, or null for none. */
|
|
174
|
+
interface SymmetryGuideSource {
|
|
175
|
+
current(): {
|
|
176
|
+
group: string;
|
|
177
|
+
spec: SymmetrySpec;
|
|
178
|
+
center: Pt$2;
|
|
179
|
+
} | null;
|
|
180
|
+
subscribe(listener: () => void): () => void;
|
|
181
|
+
}
|
|
182
|
+
/** Each handle the guide shows for `spec`, in screen pixels, topmost first. */
|
|
183
|
+
declare function guideHandles(spec: SymmetrySpec, center: Pt$2, view: View, knobPx: number): Array<{
|
|
184
|
+
handle: SymmetryHandle;
|
|
185
|
+
at: Pt$2;
|
|
186
|
+
}>;
|
|
187
|
+
/**
|
|
188
|
+
* The active group's guide: its mirror lines across the canvas, a spoke per
|
|
189
|
+
* wedge of a radial kind, a repeat's line and steps, and the handles that
|
|
190
|
+
* move the center, turn the axis and stretch a repeat. Only a handle is hit;
|
|
191
|
+
* a stroke can start anywhere else, on a line included.
|
|
192
|
+
*/
|
|
193
|
+
declare function symmetryGuideLayer(source: SymmetryGuideSource, style?: SymmetryGuideStyle): RenderLayer<unknown>;
|
|
194
|
+
|
|
195
|
+
type AnyScene = Scene<unknown, string, unknown>;
|
|
196
|
+
type Pt$1 = {
|
|
197
|
+
x: number;
|
|
198
|
+
y: number;
|
|
199
|
+
};
|
|
200
|
+
/** What changes on a group: its spec, its center, or both. */
|
|
201
|
+
interface SymmetryChange {
|
|
202
|
+
spec?: Partial<SymmetrySpec>;
|
|
203
|
+
center?: Pt$1;
|
|
204
|
+
}
|
|
205
|
+
/** Live symmetry on one scene: which group is being drawn into, the edits a
|
|
206
|
+
* group takes, and the canvas entry that wires it all up. */
|
|
207
|
+
interface SymmetryController {
|
|
208
|
+
readonly scene: AnyScene;
|
|
209
|
+
/** The group new shapes are drawn into, or null for none. Answers null
|
|
210
|
+
* once that group is gone, an undone create say. */
|
|
211
|
+
active(): NodeId | null;
|
|
212
|
+
setActive(id: NodeId | null): void;
|
|
213
|
+
/** Hear that the active group, or anything about it, changed. */
|
|
214
|
+
subscribe(listener: () => void): () => void;
|
|
215
|
+
/** Add an empty group, one undo step, and make it the active one. Its layer
|
|
216
|
+
* defaults to the scene's first. */
|
|
217
|
+
create(opts: {
|
|
218
|
+
center: Pt$1;
|
|
219
|
+
symmetry?: Partial<SymmetrySpec>;
|
|
220
|
+
layer?: string;
|
|
221
|
+
parent?: NodeId | null;
|
|
222
|
+
}): NodeId;
|
|
223
|
+
/** Group `id`'s spec and center, or null when it is not a symmetry group. */
|
|
224
|
+
read(id: NodeId | null): {
|
|
225
|
+
spec: SymmetrySpec;
|
|
226
|
+
center: Pt$1;
|
|
227
|
+
} | null;
|
|
228
|
+
/** Change a group, one undo step. */
|
|
229
|
+
update(id: NodeId, change: SymmetryChange, label?: string): void;
|
|
230
|
+
/** Change a group with no undo step, for the frames of a drag. */
|
|
231
|
+
preview(id: NodeId, change: SymmetryChange): void;
|
|
232
|
+
/** Replace a group with a plain one holding real copies. See {@link bakeSymmetry}. */
|
|
233
|
+
bake(id: NodeId): BakeResult | null;
|
|
234
|
+
/**
|
|
235
|
+
* The canvas entry, for `<SceneCanvas ambient>`: it draws the active
|
|
236
|
+
* group's guide with handles that drag, puts drawn shapes into the active
|
|
237
|
+
* group (the `insertInto` dep), and keeps a path that is its own image
|
|
238
|
+
* under its group symmetric while its anchors are edited (the
|
|
239
|
+
* `pathSymmetry` dep). Built once per controller.
|
|
240
|
+
*/
|
|
241
|
+
readonly contribution: SurfaceContribution;
|
|
242
|
+
}
|
|
243
|
+
/** Options for {@link createSymmetry}. */
|
|
244
|
+
interface CreateSymmetryOptions {
|
|
245
|
+
guide?: SymmetryGuideStyle;
|
|
246
|
+
}
|
|
247
|
+
/** Live symmetry for `scene`. Pass `symmetryRegistry` to the scene too, so its groups save. */
|
|
248
|
+
declare function createSymmetry(scene: AnyScene, opts?: CreateSymmetryOptions): SymmetryController;
|
|
249
|
+
|
|
250
|
+
/** The id of the action that drags a guide handle. */
|
|
251
|
+
declare const SYMMETRY_HANDLE_ACTION_ID = "symmetry.dragHandle";
|
|
252
|
+
type Pt = {
|
|
253
|
+
x: number;
|
|
254
|
+
y: number;
|
|
255
|
+
};
|
|
256
|
+
/** The change one frame of a handle drag makes, from where the press began. */
|
|
257
|
+
declare function handleChange(press: SymmetryHandlePress, origin: {
|
|
258
|
+
center: Pt;
|
|
259
|
+
}, from: Pt, to: Pt, stepped: boolean, snap: (p: Pt) => Pt): SymmetryChange;
|
|
260
|
+
/**
|
|
261
|
+
* Drag a handle of the active group's guide: the center moves the group's
|
|
262
|
+
* center, through the `snap` dep when there is one; the knob turns the axis;
|
|
263
|
+
* a repeat's step handle sets both its direction and its spacing. Shift holds
|
|
264
|
+
* the angle to 15° steps. The drag lands as one undo step, and cancel puts
|
|
265
|
+
* the group back.
|
|
266
|
+
*/
|
|
267
|
+
declare function handleDragAction(ctrl: SymmetryController): Action & {
|
|
268
|
+
requires: string[];
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export { type BakeResult, type CreateSymmetryOptions, DEFAULT_SYMMETRY, HORIZONTAL_AXIS, SYMMETRY_CLIP_KEY, SYMMETRY_COPIES_KEY, SYMMETRY_GUIDE_ID, SYMMETRY_HANDLE_ACTION_ID, SYMMETRY_HANDLE_KIND, SYMMETRY_KINDS, type SymmetryChange, type SymmetryController, type SymmetryGroupData, type SymmetryGroupOptions, type SymmetryGuideSource, type SymmetryGuideStyle, type SymmetryHandle, type SymmetryHandlePress, type SymmetryKind, type SymmetryPose, type SymmetrySpec, VERTICAL_AXIS, bakeSymmetry, centerOfPose, createSymmetry, editSymmetries, guideHandles, handleChange, handleDragAction, isSymmetryGroup, mirrorAcross, poseAtCenter, readGroup, specOfData, symmetryCopies, symmetryGroupOf, symmetryGroupSpec, symmetryGuideLayer, symmetryRegistry, symmetryTransforms, turnAbout };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { rotate, translate, multiply, transformPath, boundsOfPath, applyToPoint, linePath, ellipsePath, pathThroughPoseFrame, pathInPoseFrame } from '@weasel-js/geom';
|
|
2
|
+
import { hitPredicate, unionOfChildren, UNION_OF_CHILDREN, viewToTransform, worldToScreen } from '@weasel-js/core';
|
|
3
|
+
|
|
4
|
+
// src/kinds.ts
|
|
5
|
+
var VERTICAL_AXIS = Math.PI / 2;
|
|
6
|
+
var HORIZONTAL_AXIS = 0;
|
|
7
|
+
var DEFAULT_SYMMETRY = {
|
|
8
|
+
kind: "mirror",
|
|
9
|
+
angle: VERTICAL_AXIS,
|
|
10
|
+
count: 6,
|
|
11
|
+
spacing: 120,
|
|
12
|
+
glide: false
|
|
13
|
+
};
|
|
14
|
+
var exact = (v) => Math.abs(v) < 1e-12 ? 0 : v;
|
|
15
|
+
var about = (c, m) => multiply(translate(c.x, c.y), multiply(m, translate(-c.x, -c.y)));
|
|
16
|
+
function mirrorAcross(c, angle) {
|
|
17
|
+
const cos = exact(Math.cos(2 * angle));
|
|
18
|
+
const sin = exact(Math.sin(2 * angle));
|
|
19
|
+
return about(c, [cos, sin, sin, -cos, 0, 0]);
|
|
20
|
+
}
|
|
21
|
+
function turnAbout(c, angle) {
|
|
22
|
+
const [a, b, cc, d] = rotate(angle);
|
|
23
|
+
return about(c, [exact(a), exact(b), exact(cc), exact(d), 0, 0]);
|
|
24
|
+
}
|
|
25
|
+
var turns = (c, n) => Array.from({ length: Math.max(0, n - 1) }, (_, k) => turnAbout(c, 2 * Math.PI * (k + 1) / n));
|
|
26
|
+
var SYMMETRY_KINDS = {
|
|
27
|
+
mirror: { label: "Mirror", group: true, transforms: (s, c) => [mirrorAcross(c, s.angle)] },
|
|
28
|
+
both: {
|
|
29
|
+
label: "Both axes",
|
|
30
|
+
group: true,
|
|
31
|
+
transforms: (s, c) => [mirrorAcross(c, s.angle), mirrorAcross(c, s.angle + Math.PI / 2), turnAbout(c, Math.PI)]
|
|
32
|
+
},
|
|
33
|
+
point: { label: "Point", group: true, transforms: (_s, c) => [turnAbout(c, Math.PI)] },
|
|
34
|
+
radial: { label: "Radial", group: true, transforms: (s, c) => turns(c, wedges(s)) },
|
|
35
|
+
kaleidoscope: {
|
|
36
|
+
label: "Kaleidoscope",
|
|
37
|
+
group: true,
|
|
38
|
+
transforms: (s, c) => {
|
|
39
|
+
const n = wedges(s);
|
|
40
|
+
return [...turns(c, n), ...Array.from({ length: n }, (_, k) => mirrorAcross(c, s.angle + Math.PI * k / n))];
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
repeat: {
|
|
44
|
+
label: "Repeat",
|
|
45
|
+
group: false,
|
|
46
|
+
transforms: (s, c) => {
|
|
47
|
+
const dx = exact(Math.cos(s.angle)) * s.spacing;
|
|
48
|
+
const dy = exact(Math.sin(s.angle)) * s.spacing;
|
|
49
|
+
const glide = mirrorAcross(c, s.angle);
|
|
50
|
+
return Array.from({ length: Math.max(0, Math.round(s.count) - 1) }, (_, i) => {
|
|
51
|
+
const k = i + 1;
|
|
52
|
+
const step = translate(k * dx, k * dy);
|
|
53
|
+
return s.glide && k % 2 === 1 ? multiply(step, glide) : step;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
function wedges(s) {
|
|
59
|
+
return Math.max(1, Math.round(s.count));
|
|
60
|
+
}
|
|
61
|
+
function symmetryTransforms(spec, center) {
|
|
62
|
+
return SYMMETRY_KINDS[spec.kind].transforms(spec, center);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/group.ts
|
|
66
|
+
function centerOfPose(pose) {
|
|
67
|
+
const p = pose;
|
|
68
|
+
return { x: (p.x ?? 0) + (p.width ?? 0) / 2, y: (p.y ?? 0) + (p.height ?? 0) / 2 };
|
|
69
|
+
}
|
|
70
|
+
function poseAtCenter(c) {
|
|
71
|
+
return { x: c.x, y: c.y, width: 0, height: 0 };
|
|
72
|
+
}
|
|
73
|
+
function specOfData(data) {
|
|
74
|
+
const s = data?.symmetry;
|
|
75
|
+
return { ...DEFAULT_SYMMETRY, ...s };
|
|
76
|
+
}
|
|
77
|
+
var symmetryCopies = (pose, node) => symmetryTransforms(specOfData(node.data), centerOfPose(pose));
|
|
78
|
+
var drawEverywhere = () => null;
|
|
79
|
+
var SYMMETRY_COPIES_KEY = "weasel-symmetry:copies";
|
|
80
|
+
var SYMMETRY_CLIP_KEY = "weasel-symmetry:no-clip";
|
|
81
|
+
var symmetryRegistry = {
|
|
82
|
+
copiesFromPose: { [SYMMETRY_COPIES_KEY]: symmetryCopies },
|
|
83
|
+
clipFromPose: { [SYMMETRY_CLIP_KEY]: drawEverywhere }
|
|
84
|
+
};
|
|
85
|
+
function symmetryGroupSpec(opts) {
|
|
86
|
+
return {
|
|
87
|
+
kind: "container",
|
|
88
|
+
layer: opts.layer,
|
|
89
|
+
pose: poseAtCenter(opts.center),
|
|
90
|
+
data: { symmetry: { ...DEFAULT_SYMMETRY, ...opts.symmetry } },
|
|
91
|
+
clipFromPose: drawEverywhere,
|
|
92
|
+
copiesFromPose: symmetryCopies,
|
|
93
|
+
...opts.id !== void 0 ? { id: opts.id } : {},
|
|
94
|
+
...opts.parent !== void 0 ? { parent: opts.parent } : {},
|
|
95
|
+
...opts.index !== void 0 ? { index: opts.index } : {}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function isSymmetryGroup(scene, id) {
|
|
99
|
+
if (id == null) return false;
|
|
100
|
+
const node = scene.get(id);
|
|
101
|
+
return node?.kind === "container" && node.copiesFromPose === symmetryCopies;
|
|
102
|
+
}
|
|
103
|
+
function symmetryGroupOf(scene, id) {
|
|
104
|
+
let cur = id;
|
|
105
|
+
const seen = /* @__PURE__ */ new Set();
|
|
106
|
+
while (cur !== null && !seen.has(cur)) {
|
|
107
|
+
if (isSymmetryGroup(scene, cur)) return cur;
|
|
108
|
+
seen.add(cur);
|
|
109
|
+
cur = scene.get(cur)?.parent ?? null;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function readGroup(scene, id) {
|
|
114
|
+
if (!isSymmetryGroup(scene, id)) return null;
|
|
115
|
+
const node = scene.get(id);
|
|
116
|
+
return { spec: specOfData(node.data), center: centerOfPose(scene.overrides.read(node.id)?.pose ?? node.pose) };
|
|
117
|
+
}
|
|
118
|
+
function editSymmetries(scene, id) {
|
|
119
|
+
const g = readGroup(scene, id);
|
|
120
|
+
if (g === null || !SYMMETRY_KINDS[g.spec.kind].group) return [];
|
|
121
|
+
return symmetryTransforms(g.spec, g.center);
|
|
122
|
+
}
|
|
123
|
+
function worldPathOf(node, pose) {
|
|
124
|
+
const path = node.data?.path;
|
|
125
|
+
if (!path || !pose || typeof pose !== "object") return null;
|
|
126
|
+
return pathThroughPoseFrame(pathInPoseFrame(path, pose), pose);
|
|
127
|
+
}
|
|
128
|
+
function bakeSymmetry(scene, id, label = "Bake symmetry") {
|
|
129
|
+
if (!isSymmetryGroup(scene, id)) return null;
|
|
130
|
+
return scene.batch(label, () => bakeInBatch(scene, id));
|
|
131
|
+
}
|
|
132
|
+
function bakeInBatch(scene, id) {
|
|
133
|
+
const skipped = [];
|
|
134
|
+
for (const child of [...scene.childrenOf(id)]) {
|
|
135
|
+
for (const inner of [...descendantGroups(scene, child)].reverse()) skipped.push(...bakeInBatch(scene, inner).skipped);
|
|
136
|
+
}
|
|
137
|
+
const group = scene.get(id);
|
|
138
|
+
const copies = group.kind === "container" && group.copiesFromPose ? group.copiesFromPose(group.pose, group) ?? [] : [];
|
|
139
|
+
const index = (group.parent === null ? scene.roots : scene.childrenOf(group.parent)).indexOf(id);
|
|
140
|
+
const plain = scene.add({
|
|
141
|
+
kind: "container",
|
|
142
|
+
layer: group.layer,
|
|
143
|
+
pose: group.pose,
|
|
144
|
+
data: {},
|
|
145
|
+
parent: group.parent,
|
|
146
|
+
index,
|
|
147
|
+
dependsOn: "children",
|
|
148
|
+
derivePose: scene.registry.derivePose?.[UNION_OF_CHILDREN] ?? unionOfChildren
|
|
149
|
+
});
|
|
150
|
+
const children = [...scene.childrenOf(id)];
|
|
151
|
+
for (const child of children) scene.move(child, plain);
|
|
152
|
+
for (const m of copies) {
|
|
153
|
+
for (const child of children) copySubtree(scene, child, m, plain, skipped);
|
|
154
|
+
}
|
|
155
|
+
scene.remove(id);
|
|
156
|
+
return { group: plain, skipped: [...new Set(skipped)] };
|
|
157
|
+
}
|
|
158
|
+
function descendantGroups(scene, id) {
|
|
159
|
+
const out = [];
|
|
160
|
+
const walk = (n) => {
|
|
161
|
+
if (isSymmetryGroup(scene, n)) out.push(n);
|
|
162
|
+
for (const c of scene.childrenOf(n)) walk(c);
|
|
163
|
+
};
|
|
164
|
+
walk(id);
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
167
|
+
function copySubtree(scene, id, m, parent, skipped) {
|
|
168
|
+
const node = scene.get(id);
|
|
169
|
+
if (!node) return;
|
|
170
|
+
if (node.kind === "container") {
|
|
171
|
+
const sub = scene.add({
|
|
172
|
+
kind: "container",
|
|
173
|
+
layer: node.layer,
|
|
174
|
+
pose: node.pose,
|
|
175
|
+
data: node.data,
|
|
176
|
+
parent,
|
|
177
|
+
dependsOn: "children",
|
|
178
|
+
derivePose: scene.registry.derivePose?.[UNION_OF_CHILDREN] ?? unionOfChildren
|
|
179
|
+
});
|
|
180
|
+
for (const c of scene.childrenOf(id)) copySubtree(scene, c, m, sub, skipped);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const world = worldPathOf(node, node.pose);
|
|
184
|
+
if (world === null) {
|
|
185
|
+
skipped.push(id);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const path = transformPath(world, m);
|
|
189
|
+
const b = boundsOfPath(path);
|
|
190
|
+
scene.add({
|
|
191
|
+
kind: "leaf",
|
|
192
|
+
layer: node.layer,
|
|
193
|
+
pose: { x: b.x, y: b.y, width: b.width, height: b.height },
|
|
194
|
+
data: { ...node.data, path },
|
|
195
|
+
parent
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
var SYMMETRY_GUIDE_ID = "weasel-symmetry-guide";
|
|
199
|
+
var SYMMETRY_HANDLE_KIND = `layer:${SYMMETRY_GUIDE_ID}`;
|
|
200
|
+
var CURSORS = { center: "move", angle: "grab", step: "grab" };
|
|
201
|
+
function guideHandles(spec, center, view, knobPx) {
|
|
202
|
+
const t = viewToTransform(view);
|
|
203
|
+
const [cx, cy] = worldToScreen(center.x, center.y, t);
|
|
204
|
+
const out = [];
|
|
205
|
+
if (spec.kind === "repeat") {
|
|
206
|
+
const [sx, sy] = worldToScreen(
|
|
207
|
+
center.x + Math.cos(spec.angle) * spec.spacing,
|
|
208
|
+
center.y + Math.sin(spec.angle) * spec.spacing,
|
|
209
|
+
t
|
|
210
|
+
);
|
|
211
|
+
out.push({ handle: "step", at: { x: sx, y: sy } });
|
|
212
|
+
} else if (hasAxis(spec)) {
|
|
213
|
+
const d = screenDir(spec.angle, view);
|
|
214
|
+
out.push({ handle: "angle", at: { x: cx + d.x * knobPx, y: cy + d.y * knobPx } });
|
|
215
|
+
}
|
|
216
|
+
out.push({ handle: "center", at: { x: cx, y: cy } });
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
function hasAxis(spec) {
|
|
220
|
+
return spec.kind === "mirror" || spec.kind === "both" || spec.kind === "kaleidoscope";
|
|
221
|
+
}
|
|
222
|
+
function screenDir(angle, view) {
|
|
223
|
+
const x = Math.cos(angle) * view.scale.x;
|
|
224
|
+
const y = Math.sin(angle) * view.scale.y;
|
|
225
|
+
const len = Math.hypot(x, y) || 1;
|
|
226
|
+
return { x: x / len, y: y / len };
|
|
227
|
+
}
|
|
228
|
+
function mirrorAngles(spec) {
|
|
229
|
+
if (spec.kind === "mirror") return [spec.angle];
|
|
230
|
+
if (spec.kind === "both") return [spec.angle, spec.angle + Math.PI / 2];
|
|
231
|
+
if (spec.kind === "kaleidoscope") {
|
|
232
|
+
const n = Math.max(1, Math.round(spec.count));
|
|
233
|
+
return Array.from({ length: n }, (_, k) => spec.angle + Math.PI * k / n);
|
|
234
|
+
}
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
function symmetryGuideLayer(source, style = {}) {
|
|
238
|
+
const color = style.color ?? "rgba(0, 140, 255, 0.9)";
|
|
239
|
+
const faint = style.faintColor ?? "rgba(0, 140, 255, 0.35)";
|
|
240
|
+
const r = style.handleRadiusPx ?? 5;
|
|
241
|
+
const hitR = style.hitRadiusPx ?? 9;
|
|
242
|
+
const knob = style.knobDistancePx ?? 64;
|
|
243
|
+
const stroke = (c) => ({ paint: { kind: "solid", color: c }, width: { px: 1 } });
|
|
244
|
+
const line = (a, b, c) => ({ kind: "path", path: linePath(a, b), stroke: stroke(c) });
|
|
245
|
+
const dot = (at, radius) => ellipsePath({ x: at.x - radius, y: at.y - radius, width: radius * 2, height: radius * 2 });
|
|
246
|
+
return {
|
|
247
|
+
id: SYMMETRY_GUIDE_ID,
|
|
248
|
+
label: "Symmetry guide",
|
|
249
|
+
space: "screen",
|
|
250
|
+
subscribe: source.subscribe,
|
|
251
|
+
draw(_data, view, dims) {
|
|
252
|
+
const g = source.current();
|
|
253
|
+
if (!g) return [];
|
|
254
|
+
const { spec, center } = g;
|
|
255
|
+
const t = viewToTransform(view);
|
|
256
|
+
const [cx, cy] = worldToScreen(center.x, center.y, t);
|
|
257
|
+
const reach = Math.hypot(dims.width, dims.height) * 2;
|
|
258
|
+
const out = [];
|
|
259
|
+
for (const a of mirrorAngles(spec)) {
|
|
260
|
+
const d = screenDir(a, view);
|
|
261
|
+
out.push(line({ x: cx - d.x * reach, y: cy - d.y * reach }, { x: cx + d.x * reach, y: cy + d.y * reach }, color));
|
|
262
|
+
}
|
|
263
|
+
if (spec.kind === "radial") {
|
|
264
|
+
const n = Math.max(1, Math.round(spec.count));
|
|
265
|
+
for (let k = 0; k < n; k++) {
|
|
266
|
+
const d = screenDir(spec.angle + 2 * Math.PI * k / n, view);
|
|
267
|
+
out.push(line({ x: cx, y: cy }, { x: cx + d.x * reach, y: cy + d.y * reach }, faint));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (spec.kind === "repeat") {
|
|
271
|
+
const d = screenDir(spec.angle, view);
|
|
272
|
+
out.push(line({ x: cx, y: cy }, { x: cx + d.x * reach, y: cy + d.y * reach }, faint));
|
|
273
|
+
for (const m of SYMMETRY_KINDS.repeat.transforms(spec, center)) {
|
|
274
|
+
const [wx, wy] = applyToPoint(m, center.x, center.y);
|
|
275
|
+
const [x, y] = worldToScreen(wx, wy, t);
|
|
276
|
+
out.push({ kind: "path", path: dot({ x, y }, 2.5), fill: { paint: { kind: "solid", color: faint } } });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const h of guideHandles(spec, center, view, knob)) {
|
|
280
|
+
if (h.handle === "angle") out.push(line({ x: cx, y: cy }, h.at, color));
|
|
281
|
+
out.push({
|
|
282
|
+
kind: "path",
|
|
283
|
+
path: dot(h.at, h.handle === "center" ? r + 1 : r),
|
|
284
|
+
fill: { paint: { kind: "solid", color: h.handle === "center" ? color : "#fff" } },
|
|
285
|
+
stroke: { paint: { kind: "solid", color: h.handle === "center" ? "#fff" : color }, width: { px: 1.5 } }
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
},
|
|
290
|
+
hitTest(worldX, worldY, _data, view) {
|
|
291
|
+
const g = source.current();
|
|
292
|
+
if (!g) return null;
|
|
293
|
+
const [px, py] = worldToScreen(worldX, worldY, viewToTransform(view));
|
|
294
|
+
for (const h of guideHandles(g.spec, g.center, view, knob)) {
|
|
295
|
+
if (Math.hypot(px - h.at.x, py - h.at.y) > hitR) continue;
|
|
296
|
+
const press = { handle: h.handle, group: g.group };
|
|
297
|
+
return { strength: "exclusive", cursor: CURSORS[h.handle], initialScratch: press };
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/handleDrag.ts
|
|
305
|
+
var SYMMETRY_HANDLE_ACTION_ID = "symmetry.dragHandle";
|
|
306
|
+
var ANGLE_STEP = Math.PI / 12;
|
|
307
|
+
function handleChange(press, origin, from, to, stepped, snap) {
|
|
308
|
+
if (press.handle === "center") {
|
|
309
|
+
return { center: snap({ x: origin.center.x + to.x - from.x, y: origin.center.y + to.y - from.y }) };
|
|
310
|
+
}
|
|
311
|
+
const dx = to.x - origin.center.x;
|
|
312
|
+
const dy = to.y - origin.center.y;
|
|
313
|
+
let angle = Math.atan2(dy, dx);
|
|
314
|
+
if (stepped) angle = Math.round(angle / ANGLE_STEP) * ANGLE_STEP;
|
|
315
|
+
if (press.handle === "angle") return { spec: { angle } };
|
|
316
|
+
return { spec: { angle, spacing: Math.max(1, Math.hypot(dx, dy)) } };
|
|
317
|
+
}
|
|
318
|
+
function handleDragAction(ctrl) {
|
|
319
|
+
return {
|
|
320
|
+
id: SYMMETRY_HANDLE_ACTION_ID,
|
|
321
|
+
label: "Move symmetry guide",
|
|
322
|
+
requires: ["snap"],
|
|
323
|
+
invoker: {
|
|
324
|
+
timing: "ongoing",
|
|
325
|
+
start(ctx) {
|
|
326
|
+
const press = ctx.drag?.affordance?.payload;
|
|
327
|
+
if (!press) return {};
|
|
328
|
+
const id = press.group;
|
|
329
|
+
const origin = ctrl.read(id);
|
|
330
|
+
if (!origin) return {};
|
|
331
|
+
const snapDep = ctx.deps.snap;
|
|
332
|
+
const snap = (p) => snapDep ? snapDep.point(p) : p;
|
|
333
|
+
const from = { x: ctx.world.x, y: ctx.world.y };
|
|
334
|
+
let last = null;
|
|
335
|
+
const back = { spec: origin.spec, center: origin.center };
|
|
336
|
+
return {
|
|
337
|
+
kind: "symmetry-guide",
|
|
338
|
+
onMove(move) {
|
|
339
|
+
last = handleChange(press, origin, from, move.world, move.modifiers.shift, snap);
|
|
340
|
+
ctrl.preview(id, last);
|
|
341
|
+
},
|
|
342
|
+
onEnd(_end, reason) {
|
|
343
|
+
ctrl.preview(id, back);
|
|
344
|
+
if (reason === "commit" && last) ctrl.update(id, last, "Move symmetry guide");
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
var GRAB_HANDLE_ID = "symmetry.grabHandle";
|
|
352
|
+
var grabHandle = {
|
|
353
|
+
id: GRAB_HANDLE_ID,
|
|
354
|
+
label: "Grab symmetry handle",
|
|
355
|
+
invoker: { timing: "immediate", run: () => {
|
|
356
|
+
} },
|
|
357
|
+
enabled: () => true
|
|
358
|
+
};
|
|
359
|
+
var MODS_ANY = { shift: "optional", alt: "optional", meta: "optional", ctrl: "optional" };
|
|
360
|
+
var onHandle = hitPredicate((hit) => hit?.kind === SYMMETRY_HANDLE_KIND);
|
|
361
|
+
function createSymmetry(scene, opts = {}) {
|
|
362
|
+
let activeId = null;
|
|
363
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
364
|
+
const notify = () => {
|
|
365
|
+
for (const l of [...listeners]) l();
|
|
366
|
+
};
|
|
367
|
+
const write = (id, change) => {
|
|
368
|
+
const node = scene.get(id);
|
|
369
|
+
if (!node || !isSymmetryGroup(scene, id)) return;
|
|
370
|
+
if (change.spec) scene.update(id, { data: { ...node.data, symmetry: { ...specOfData(node.data), ...change.spec } } });
|
|
371
|
+
if (change.center) scene.setPose(id, { ...node.pose, ...poseAtCenter(change.center) });
|
|
372
|
+
};
|
|
373
|
+
const ctrl = {
|
|
374
|
+
scene,
|
|
375
|
+
active: () => isSymmetryGroup(scene, activeId) ? activeId : null,
|
|
376
|
+
setActive(id) {
|
|
377
|
+
activeId = id;
|
|
378
|
+
notify();
|
|
379
|
+
},
|
|
380
|
+
subscribe(listener) {
|
|
381
|
+
listeners.add(listener);
|
|
382
|
+
return () => {
|
|
383
|
+
listeners.delete(listener);
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
create({ center, symmetry, layer, parent }) {
|
|
387
|
+
const id = scene.add(symmetryGroupSpec({
|
|
388
|
+
layer: layer ?? scene.layers[0]?.id ?? "default",
|
|
389
|
+
center,
|
|
390
|
+
...symmetry ? { symmetry } : {},
|
|
391
|
+
...parent !== void 0 ? { parent } : {}
|
|
392
|
+
}));
|
|
393
|
+
ctrl.setActive(id);
|
|
394
|
+
return id;
|
|
395
|
+
},
|
|
396
|
+
read: (id) => readGroup(scene, id),
|
|
397
|
+
update(id, change, label = "Change symmetry") {
|
|
398
|
+
scene.batch(label, () => write(id, change));
|
|
399
|
+
notify();
|
|
400
|
+
},
|
|
401
|
+
preview(id, change) {
|
|
402
|
+
scene.untracked(() => write(id, change));
|
|
403
|
+
notify();
|
|
404
|
+
},
|
|
405
|
+
bake(id) {
|
|
406
|
+
const out = bakeSymmetry(scene, id);
|
|
407
|
+
notify();
|
|
408
|
+
return out;
|
|
409
|
+
},
|
|
410
|
+
contribution: void 0
|
|
411
|
+
};
|
|
412
|
+
const guide = symmetryGuideLayer({
|
|
413
|
+
current() {
|
|
414
|
+
const id = ctrl.active();
|
|
415
|
+
const g = readGroup(scene, id);
|
|
416
|
+
return g === null || id === null ? null : { group: id, ...g };
|
|
417
|
+
},
|
|
418
|
+
subscribe: ctrl.subscribe
|
|
419
|
+
}, opts.guide);
|
|
420
|
+
const action = handleDragAction(ctrl);
|
|
421
|
+
ctrl.contribution = {
|
|
422
|
+
id: "weasel-symmetry",
|
|
423
|
+
eligibility: { claimed: true },
|
|
424
|
+
deps: {
|
|
425
|
+
insertInto: () => ({ get: () => ctrl.active() }),
|
|
426
|
+
pathSymmetry: () => ({ transforms: (pathId) => editSymmetries(scene, symmetryGroupOf(scene, pathId)) })
|
|
427
|
+
},
|
|
428
|
+
actions: [action, grabHandle],
|
|
429
|
+
bindings: [
|
|
430
|
+
{ spec: { kind: "drag", target: { kindOf: onHandle }, mods: MODS_ANY }, actionId: SYMMETRY_HANDLE_ACTION_ID },
|
|
431
|
+
// A handle's claim bars every other binding for the whole press, so its
|
|
432
|
+
// pointer-down and a click that never becomes a drag need one too.
|
|
433
|
+
{ spec: { kind: "pointerDown", target: `affordance:${SYMMETRY_HANDLE_KIND}` }, actionId: GRAB_HANDLE_ID },
|
|
434
|
+
{ spec: { kind: "click", target: `affordance:${SYMMETRY_HANDLE_KIND}` }, actionId: GRAB_HANDLE_ID }
|
|
435
|
+
],
|
|
436
|
+
attach: (api) => {
|
|
437
|
+
const off = api.registerLayer(guide);
|
|
438
|
+
api.requestRedraw();
|
|
439
|
+
return off;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
return ctrl;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export { DEFAULT_SYMMETRY, HORIZONTAL_AXIS, SYMMETRY_CLIP_KEY, SYMMETRY_COPIES_KEY, SYMMETRY_GUIDE_ID, SYMMETRY_HANDLE_ACTION_ID, SYMMETRY_HANDLE_KIND, SYMMETRY_KINDS, VERTICAL_AXIS, bakeSymmetry, centerOfPose, createSymmetry, editSymmetries, guideHandles, handleChange, handleDragAction, isSymmetryGroup, mirrorAcross, poseAtCenter, readGroup, specOfData, symmetryCopies, symmetryGroupOf, symmetryGroupSpec, symmetryGuideLayer, symmetryRegistry, symmetryTransforms, turnAbout };
|
|
446
|
+
//# sourceMappingURL=index.js.map
|
|
447
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/kinds.ts","../src/group.ts","../src/bake.ts","../src/guide.ts","../src/handleDrag.ts","../src/controller.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { multiply, rotate, translate, type Affine } from '@weasel-js/geom';\n\n/** Which copies a symmetry group draws. */\nexport type SymmetryKind = 'mirror' | 'both' | 'point' | 'radial' | 'kaleidoscope' | 'repeat';\n\n/**\n * Everything a symmetry group's copies are built from, besides its center.\n * Each kind reads only the fields it needs, so switching kind keeps the rest.\n */\nexport interface SymmetrySpec {\n kind: SymmetryKind;\n /**\n * Radians, measured from +x toward +y, which is clockwise on screen. The\n * axis a mirror reflects across, the first of `both`'s two axes and of a\n * kaleidoscope's mirrors, and the direction a repeat steps in.\n * {@link VERTICAL_AXIS} mirrors left and right, {@link HORIZONTAL_AXIS}\n * top and bottom.\n */\n angle: number;\n /** How many there are, the original included: wedges round the center for\n * `radial` and `kaleidoscope`, places along the line for `repeat`. */\n count: number;\n /** World distance from one repeat to the next. */\n spacing: number;\n /** `repeat` only: every other repeat is mirrored across the line it steps\n * along, a glide reflection. */\n glide: boolean;\n}\n\n/** The angle of an axis running up and down the screen. */\nexport const VERTICAL_AXIS = Math.PI / 2;\n/** The angle of an axis running across the screen. */\nexport const HORIZONTAL_AXIS = 0;\n\n/** A mirror across the vertical axis, with the other fields at their defaults. */\nexport const DEFAULT_SYMMETRY: SymmetrySpec = {\n kind: 'mirror',\n angle: VERTICAL_AXIS,\n count: 6,\n spacing: 120,\n glide: false,\n};\n\ntype Pt = { x: number; y: number };\n\n/** `Math.cos` and `Math.sin` leave 1e-16 where a quarter turn has an exact\n * 0, which a preset axis then carries into every coordinate it mirrors. */\nconst exact = (v: number): number => (Math.abs(v) < 1e-12 ? 0 : v);\n\nconst about = (c: Pt, m: Affine): Affine => multiply(translate(c.x, c.y), multiply(m, translate(-c.x, -c.y)));\n\n/** The reflection across the line through `c` at `angle`. */\nexport function mirrorAcross(c: Pt, angle: number): Affine {\n const cos = exact(Math.cos(2 * angle));\n const sin = exact(Math.sin(2 * angle));\n return about(c, [cos, sin, sin, -cos, 0, 0]);\n}\n\n/** The rotation by `angle` about `c`. */\nexport function turnAbout(c: Pt, angle: number): Affine {\n const [a, b, cc, d] = rotate(angle);\n return about(c, [exact(a), exact(b), exact(cc), exact(d), 0, 0]);\n}\n\nconst turns = (c: Pt, n: number): Affine[] =>\n Array.from({ length: Math.max(0, n - 1) }, (_, k) => turnAbout(c, (2 * Math.PI * (k + 1)) / n));\n\n/** One kind: how its copies are built, and whether they form a finite group —\n * one a path can be its own image under, which a repeat's never can. */\ninterface KindDef {\n readonly label: string;\n readonly group: boolean;\n transforms(spec: SymmetrySpec, c: Pt): Affine[];\n}\n\n/**\n * Every kind, by name. A new kind, a wallpaper tiling say, is a row here: the\n * group, the guide layer and the controls all read the table.\n */\nexport const SYMMETRY_KINDS: Readonly<Record<SymmetryKind, KindDef>> = {\n mirror: { label: 'Mirror', group: true, transforms: (s, c) => [mirrorAcross(c, s.angle)] },\n both: {\n label: 'Both axes',\n group: true,\n transforms: (s, c) => [mirrorAcross(c, s.angle), mirrorAcross(c, s.angle + Math.PI / 2), turnAbout(c, Math.PI)],\n },\n point: { label: 'Point', group: true, transforms: (_s, c) => [turnAbout(c, Math.PI)] },\n radial: { label: 'Radial', group: true, transforms: (s, c) => turns(c, wedges(s)) },\n kaleidoscope: {\n label: 'Kaleidoscope',\n group: true,\n transforms: (s, c) => {\n const n = wedges(s);\n return [...turns(c, n), ...Array.from({ length: n }, (_, k) => mirrorAcross(c, s.angle + (Math.PI * k) / n))];\n },\n },\n repeat: {\n label: 'Repeat',\n group: false,\n transforms: (s, c) => {\n const dx = exact(Math.cos(s.angle)) * s.spacing;\n const dy = exact(Math.sin(s.angle)) * s.spacing;\n const glide = mirrorAcross(c, s.angle);\n return Array.from({ length: Math.max(0, Math.round(s.count) - 1) }, (_, i) => {\n const k = i + 1;\n const step = translate(k * dx, k * dy);\n return s.glide && k % 2 === 1 ? multiply(step, glide) : step;\n });\n },\n },\n};\n\n/** The wedge count a radial kind uses: at least one, a whole number. */\nfunction wedges(s: SymmetrySpec): number {\n return Math.max(1, Math.round(s.count));\n}\n\n/**\n * The copies `spec` draws about `center`, as world affines, the original not\n * among them. For a kind that forms a group this is the whole group less its\n * identity.\n */\nexport function symmetryTransforms(spec: SymmetrySpec, center: Pt): Affine[] {\n return SYMMETRY_KINDS[spec.kind].transforms(spec, center);\n}\n","import type { Affine } from '@weasel-js/geom';\nimport type { AddNodeSpec, CopiesFromPoseFn, NodeId, Scene, SceneRegistry } from '@weasel-js/core';\nimport { DEFAULT_SYMMETRY, SYMMETRY_KINDS, symmetryTransforms, type SymmetrySpec } from './kinds';\n\n/** What a symmetry group keeps in its `data`. Its center is its pose. */\nexport interface SymmetryGroupData {\n symmetry: SymmetrySpec;\n}\n\n/** The box a symmetry group is posed at: empty, at its center. */\nexport interface SymmetryPose {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\ntype Pt = { x: number; y: number };\n\n/** Where a group's copies turn and reflect about: the middle of its pose. */\nexport function centerOfPose(pose: unknown): Pt {\n const p = pose as Partial<SymmetryPose>;\n return { x: (p.x ?? 0) + (p.width ?? 0) / 2, y: (p.y ?? 0) + (p.height ?? 0) / 2 };\n}\n\n/** The pose that puts a group's center at `c`. */\nexport function poseAtCenter(c: Pt): SymmetryPose {\n return { x: c.x, y: c.y, width: 0, height: 0 };\n}\n\n/** The spec a group's `data` holds, with any field it lacks at its default. */\nexport function specOfData(data: unknown): SymmetrySpec {\n const s = (data as Partial<SymmetryGroupData> | null | undefined)?.symmetry;\n return { ...DEFAULT_SYMMETRY, ...s };\n}\n\n/** A symmetry group's copies: the `copiesFromPose` every group carries. */\nexport const symmetryCopies: CopiesFromPoseFn<unknown> = (pose, node) =>\n symmetryTransforms(specOfData(node.data), centerOfPose(pose));\n\n/** A group draws its copies wherever they land, so it clips nothing. */\nconst drawEverywhere = (): null => null;\n\n/** The registry key a group's `copiesFromPose` is saved under. */\nexport const SYMMETRY_COPIES_KEY = 'weasel-symmetry:copies';\n/** The registry key a group's `clipFromPose`, which clips nothing, is saved under. */\nexport const SYMMETRY_CLIP_KEY = 'weasel-symmetry:no-clip';\n\n/**\n * The entries a scene needs to save and restore symmetry groups: pass it to\n * `createScene({ registry })`, merged with any of your own. Without it a\n * group still draws its copies, but `toJSON` refuses the scene.\n */\nexport const symmetryRegistry: SceneRegistry<unknown> = {\n copiesFromPose: { [SYMMETRY_COPIES_KEY]: symmetryCopies },\n clipFromPose: { [SYMMETRY_CLIP_KEY]: drawEverywhere },\n};\n\n/** What {@link symmetryGroupSpec} builds a group from. */\nexport interface SymmetryGroupOptions<TLayer extends string = string> {\n layer: TLayer;\n center: Pt;\n symmetry?: Partial<SymmetrySpec>;\n id?: NodeId;\n parent?: NodeId | null;\n index?: number;\n}\n\n/** The `scene.add` spec for a new, empty symmetry group. */\nexport function symmetryGroupSpec<TLayer extends string>(\n opts: SymmetryGroupOptions<TLayer>,\n): AddNodeSpec<SymmetryGroupData, TLayer, SymmetryPose> {\n return {\n kind: 'container',\n layer: opts.layer,\n pose: poseAtCenter(opts.center),\n data: { symmetry: { ...DEFAULT_SYMMETRY, ...opts.symmetry } },\n clipFromPose: drawEverywhere,\n copiesFromPose: symmetryCopies as CopiesFromPoseFn<SymmetryPose, SymmetryGroupData, TLayer>,\n ...(opts.id !== undefined ? { id: opts.id } : {}),\n ...(opts.parent !== undefined ? { parent: opts.parent } : {}),\n ...(opts.index !== undefined ? { index: opts.index } : {}),\n };\n}\n\ntype AnyScene = Scene<unknown, string, unknown>;\n\n/** Whether `id` names a symmetry group in `scene`. */\nexport function isSymmetryGroup(scene: AnyScene, id: NodeId | string | null | undefined): boolean {\n if (id == null) return false;\n const node = scene.get(id as NodeId);\n return node?.kind === 'container' && node.copiesFromPose === symmetryCopies;\n}\n\n/** The nearest symmetry group holding `id`, or `id` itself when it is one. */\nexport function symmetryGroupOf(scene: AnyScene, id: NodeId | string): NodeId | null {\n let cur: NodeId | null = id as NodeId;\n const seen = new Set<string>();\n while (cur !== null && !seen.has(cur)) {\n if (isSymmetryGroup(scene, cur)) return cur;\n seen.add(cur);\n cur = scene.get(cur)?.parent ?? null;\n }\n return null;\n}\n\n/** The spec and center of group `id`, or `null` when it is not one. */\nexport function readGroup(scene: AnyScene, id: NodeId | string | null): { spec: SymmetrySpec; center: Pt } | null {\n if (!isSymmetryGroup(scene, id)) return null;\n const node = scene.get(id as NodeId)!;\n return { spec: specOfData(node.data), center: centerOfPose(scene.overrides.read(node.id)?.pose ?? node.pose) };\n}\n\n/**\n * Every transform a path inside group `id` should stay its own image under\n * while it is edited, or none for a kind that is not a group. In world\n * coordinates, for the `pathSymmetry` dep.\n */\nexport function editSymmetries(scene: AnyScene, id: NodeId | string | null): Affine[] {\n const g = readGroup(scene, id);\n if (g === null || !SYMMETRY_KINDS[g.spec.kind].group) return [];\n return symmetryTransforms(g.spec, g.center);\n}\n","import {\n boundsOfPath, pathInPoseFrame, pathThroughPoseFrame, transformPath, type Affine, type Path,\n} from '@weasel-js/geom';\nimport { UNION_OF_CHILDREN, unionOfChildren, type NodeId, type Scene, type SceneNode } from '@weasel-js/core';\nimport { isSymmetryGroup } from './group';\n\ntype AnyScene = Scene<unknown, string, unknown>;\ntype AnyNode = SceneNode<unknown, string, unknown>;\n\n/** What {@link bakeSymmetry} did. */\nexport interface BakeResult {\n /** The plain group that replaced the symmetry group. */\n group: NodeId;\n /** Leaves drawn by something other than a path in their data — text, an\n * image — which kept their place but whose copies were not made. */\n skipped: NodeId[];\n}\n\n/** A path leaf's outline in world coordinates, or null for a leaf without one. */\nfunction worldPathOf(node: AnyNode, pose: unknown): Path | null {\n const path = (node.data as { path?: Path } | null)?.path;\n if (!path || !pose || typeof pose !== 'object') return null;\n return pathThroughPoseFrame(pathInPoseFrame(path, pose as never), pose);\n}\n\n/**\n * Turn symmetry group `id` into a plain group holding its children and, for\n * every copy it drew, a real node: one undo step. A symmetry group nested\n * inside is baked first. Copies are made of path leaves, their outline\n * carried through the copy's transform into a pose of its own; a leaf drawn\n * any other way is reported in `skipped`. Returns `null` when `id` is not a\n * symmetry group.\n *\n * Written for a scene of absolute poses, the kit's default, and for the\n * kit's path data, `{ path, fill?, stroke? }`. A paint that is laid out in\n * the shape's own box, a gradient say, is copied as it is.\n */\nexport function bakeSymmetry(scene: AnyScene, id: NodeId, label = 'Bake symmetry'): BakeResult | null {\n if (!isSymmetryGroup(scene, id)) return null;\n return scene.batch(label, () => bakeInBatch(scene, id));\n}\n\nfunction bakeInBatch(scene: AnyScene, id: NodeId): BakeResult {\n const skipped: NodeId[] = [];\n for (const child of [...scene.childrenOf(id)]) {\n for (const inner of [...descendantGroups(scene, child)].reverse()) skipped.push(...bakeInBatch(scene, inner).skipped);\n }\n const group = scene.get(id)!;\n const copies = group.kind === 'container' && group.copiesFromPose\n ? group.copiesFromPose(group.pose, group) ?? [] : [];\n const index = (group.parent === null ? scene.roots : scene.childrenOf(group.parent)).indexOf(id);\n const plain = scene.add({\n kind: 'container',\n layer: group.layer,\n pose: group.pose,\n data: {},\n parent: group.parent,\n index,\n dependsOn: 'children',\n derivePose: scene.registry.derivePose?.[UNION_OF_CHILDREN] ?? unionOfChildren,\n });\n const children = [...scene.childrenOf(id)];\n for (const child of children) scene.move(child, plain);\n for (const m of copies) {\n for (const child of children) copySubtree(scene, child, m, plain, skipped);\n }\n scene.remove(id);\n return { group: plain, skipped: [...new Set(skipped)] };\n}\n\nfunction descendantGroups(scene: AnyScene, id: NodeId): NodeId[] {\n const out: NodeId[] = [];\n const walk = (n: NodeId): void => {\n if (isSymmetryGroup(scene, n)) out.push(n);\n for (const c of scene.childrenOf(n)) walk(c);\n };\n walk(id);\n return out;\n}\n\n/** Add `id`, carried through `m`, under `parent`. */\nfunction copySubtree(scene: AnyScene, id: NodeId, m: Affine, parent: NodeId, skipped: NodeId[]): void {\n const node = scene.get(id);\n if (!node) return;\n if (node.kind === 'container') {\n const sub = scene.add({\n kind: 'container',\n layer: node.layer,\n pose: node.pose,\n data: node.data,\n parent,\n dependsOn: 'children',\n derivePose: scene.registry.derivePose?.[UNION_OF_CHILDREN] ?? unionOfChildren,\n });\n for (const c of scene.childrenOf(id)) copySubtree(scene, c, m, sub, skipped);\n return;\n }\n const world = worldPathOf(node, node.pose);\n if (world === null) {\n skipped.push(id);\n return;\n }\n const path = transformPath(world, m);\n const b = boundsOfPath(path);\n scene.add({\n kind: 'leaf',\n layer: node.layer,\n pose: { x: b.x, y: b.y, width: b.width, height: b.height },\n data: { ...(node.data as object), path },\n parent,\n });\n}\n","import { applyToPoint, ellipsePath, linePath, type Path } from '@weasel-js/geom';\nimport {\n viewToTransform, worldToScreen, type DrawCommand, type LayerHit, type RenderLayer, type View,\n} from '@weasel-js/core';\nimport { SYMMETRY_KINDS, type SymmetrySpec } from './kinds';\n\n/** The guide layer's id. */\nexport const SYMMETRY_GUIDE_ID = 'weasel-symmetry-guide';\n/** The affordance kind a press on one of the guide's handles arrives as. */\nexport const SYMMETRY_HANDLE_KIND = `layer:${SYMMETRY_GUIDE_ID}`;\n\n/** Which handle a press took: the center, the knob that turns the axis, or\n * the end of a repeat's first step. */\nexport type SymmetryHandle = 'center' | 'angle' | 'step';\n\n/** What a press on a handle carries to the drag action. */\nexport interface SymmetryHandlePress {\n handle: SymmetryHandle;\n group: string;\n}\n\n/** How the guide looks. Sizes are screen pixels. */\nexport interface SymmetryGuideStyle {\n color?: string;\n /** Spokes and repeat ticks, which say where copies go rather than what reflects. */\n faintColor?: string;\n handleRadiusPx?: number;\n /** How far from a handle a press still takes it. */\n hitRadiusPx?: number;\n /** How far from the center the angle knob sits. */\n knobDistancePx?: number;\n}\n\ntype Pt = { x: number; y: number };\n\n/** What the guide draws: the active group's spec and center, or null for none. */\nexport interface SymmetryGuideSource {\n current(): { group: string; spec: SymmetrySpec; center: Pt } | null;\n subscribe(listener: () => void): () => void;\n}\n\nconst CURSORS: Record<SymmetryHandle, string> = { center: 'move', angle: 'grab', step: 'grab' };\n\n/** Each handle the guide shows for `spec`, in screen pixels, topmost first. */\nexport function guideHandles(\n spec: SymmetrySpec,\n center: Pt,\n view: View,\n knobPx: number,\n): Array<{ handle: SymmetryHandle; at: Pt }> {\n const t = viewToTransform(view);\n const [cx, cy] = worldToScreen(center.x, center.y, t);\n const out: Array<{ handle: SymmetryHandle; at: Pt }> = [];\n if (spec.kind === 'repeat') {\n const [sx, sy] = worldToScreen(\n center.x + Math.cos(spec.angle) * spec.spacing, center.y + Math.sin(spec.angle) * spec.spacing, t,\n );\n out.push({ handle: 'step', at: { x: sx, y: sy } });\n } else if (hasAxis(spec)) {\n const d = screenDir(spec.angle, view);\n out.push({ handle: 'angle', at: { x: cx + d.x * knobPx, y: cy + d.y * knobPx } });\n }\n out.push({ handle: 'center', at: { x: cx, y: cy } });\n return out;\n}\n\n/** Whether the kind has an axis whose angle changes its copies. */\nfunction hasAxis(spec: SymmetrySpec): boolean {\n return spec.kind === 'mirror' || spec.kind === 'both' || spec.kind === 'kaleidoscope';\n}\n\n/** A world direction at `angle`, as a unit vector on screen. */\nfunction screenDir(angle: number, view: View): Pt {\n const x = Math.cos(angle) * view.scale.x;\n const y = Math.sin(angle) * view.scale.y;\n const len = Math.hypot(x, y) || 1;\n return { x: x / len, y: y / len };\n}\n\n/** The angles of the lines a spec reflects across. */\nfunction mirrorAngles(spec: SymmetrySpec): number[] {\n if (spec.kind === 'mirror') return [spec.angle];\n if (spec.kind === 'both') return [spec.angle, spec.angle + Math.PI / 2];\n if (spec.kind === 'kaleidoscope') {\n const n = Math.max(1, Math.round(spec.count));\n return Array.from({ length: n }, (_, k) => spec.angle + (Math.PI * k) / n);\n }\n return [];\n}\n\n/**\n * The active group's guide: its mirror lines across the canvas, a spoke per\n * wedge of a radial kind, a repeat's line and steps, and the handles that\n * move the center, turn the axis and stretch a repeat. Only a handle is hit;\n * a stroke can start anywhere else, on a line included.\n */\nexport function symmetryGuideLayer(source: SymmetryGuideSource, style: SymmetryGuideStyle = {}): RenderLayer<unknown> {\n const color = style.color ?? 'rgba(0, 140, 255, 0.9)';\n const faint = style.faintColor ?? 'rgba(0, 140, 255, 0.35)';\n const r = style.handleRadiusPx ?? 5;\n const hitR = style.hitRadiusPx ?? 9;\n const knob = style.knobDistancePx ?? 64;\n const stroke = (c: string) => ({ paint: { kind: 'solid' as const, color: c }, width: { px: 1 } });\n const line = (a: Pt, b: Pt, c: string): DrawCommand => ({ kind: 'path', path: linePath(a, b), stroke: stroke(c) });\n const dot = (at: Pt, radius: number): Path => ellipsePath({ x: at.x - radius, y: at.y - radius, width: radius * 2, height: radius * 2 });\n\n return {\n id: SYMMETRY_GUIDE_ID,\n label: 'Symmetry guide',\n space: 'screen',\n subscribe: source.subscribe,\n draw(_data, view, dims): DrawCommand[] {\n const g = source.current();\n if (!g) return [];\n const { spec, center } = g;\n const t = viewToTransform(view);\n const [cx, cy] = worldToScreen(center.x, center.y, t);\n const reach = Math.hypot(dims.width, dims.height) * 2;\n const out: DrawCommand[] = [];\n for (const a of mirrorAngles(spec)) {\n const d = screenDir(a, view);\n out.push(line({ x: cx - d.x * reach, y: cy - d.y * reach }, { x: cx + d.x * reach, y: cy + d.y * reach }, color));\n }\n if (spec.kind === 'radial') {\n const n = Math.max(1, Math.round(spec.count));\n for (let k = 0; k < n; k++) {\n const d = screenDir(spec.angle + (2 * Math.PI * k) / n, view);\n out.push(line({ x: cx, y: cy }, { x: cx + d.x * reach, y: cy + d.y * reach }, faint));\n }\n }\n if (spec.kind === 'repeat') {\n const d = screenDir(spec.angle, view);\n out.push(line({ x: cx, y: cy }, { x: cx + d.x * reach, y: cy + d.y * reach }, faint));\n for (const m of SYMMETRY_KINDS.repeat.transforms(spec, center)) {\n const [wx, wy] = applyToPoint(m, center.x, center.y);\n const [x, y] = worldToScreen(wx, wy, t);\n out.push({ kind: 'path', path: dot({ x, y }, 2.5), fill: { paint: { kind: 'solid', color: faint } } });\n }\n }\n for (const h of guideHandles(spec, center, view, knob)) {\n if (h.handle === 'angle') out.push(line({ x: cx, y: cy }, h.at, color));\n out.push({\n kind: 'path',\n path: dot(h.at, h.handle === 'center' ? r + 1 : r),\n fill: { paint: { kind: 'solid', color: h.handle === 'center' ? color : '#fff' } },\n stroke: { paint: { kind: 'solid', color: h.handle === 'center' ? '#fff' : color }, width: { px: 1.5 } },\n });\n }\n return out;\n },\n hitTest(worldX, worldY, _data, view): LayerHit | null {\n const g = source.current();\n if (!g) return null;\n const [px, py] = worldToScreen(worldX, worldY, viewToTransform(view));\n for (const h of guideHandles(g.spec, g.center, view, knob)) {\n if (Math.hypot(px - h.at.x, py - h.at.y) > hitR) continue;\n const press: SymmetryHandlePress = { handle: h.handle, group: g.group };\n return { strength: 'exclusive', cursor: CURSORS[h.handle], initialScratch: press };\n }\n return null;\n },\n };\n}\n","import type { Action, InvocationCtx, NodeId, OngoingHandle } from '@weasel-js/core';\nimport type { SymmetryChange, SymmetryController } from './controller';\nimport type { SymmetryHandlePress } from './guide';\n\n/** The id of the action that drags a guide handle. */\nexport const SYMMETRY_HANDLE_ACTION_ID = 'symmetry.dragHandle';\n\n/** Shift holds an axis to steps of this many radians. */\nconst ANGLE_STEP = Math.PI / 12;\n\ntype Pt = { x: number; y: number };\n\n/** The change one frame of a handle drag makes, from where the press began. */\nexport function handleChange(\n press: SymmetryHandlePress,\n origin: { center: Pt },\n from: Pt,\n to: Pt,\n stepped: boolean,\n snap: (p: Pt) => Pt,\n): SymmetryChange {\n if (press.handle === 'center') {\n return { center: snap({ x: origin.center.x + to.x - from.x, y: origin.center.y + to.y - from.y }) };\n }\n const dx = to.x - origin.center.x;\n const dy = to.y - origin.center.y;\n let angle = Math.atan2(dy, dx);\n if (stepped) angle = Math.round(angle / ANGLE_STEP) * ANGLE_STEP;\n if (press.handle === 'angle') return { spec: { angle } };\n return { spec: { angle, spacing: Math.max(1, Math.hypot(dx, dy)) } };\n}\n\n/**\n * Drag a handle of the active group's guide: the center moves the group's\n * center, through the `snap` dep when there is one; the knob turns the axis;\n * a repeat's step handle sets both its direction and its spacing. Shift holds\n * the angle to 15° steps. The drag lands as one undo step, and cancel puts\n * the group back.\n */\nexport function handleDragAction(ctrl: SymmetryController): Action & { requires: string[] } {\n return {\n id: SYMMETRY_HANDLE_ACTION_ID,\n label: 'Move symmetry guide',\n requires: ['snap'],\n invoker: {\n timing: 'ongoing',\n start(ctx: InvocationCtx): OngoingHandle {\n const press = ctx.drag?.affordance?.payload as SymmetryHandlePress | undefined;\n if (!press) return {};\n const id = press.group as NodeId;\n const origin = ctrl.read(id);\n if (!origin) return {};\n const snapDep = ctx.deps.snap;\n const snap = (p: Pt) => (snapDep ? snapDep.point(p) : p);\n const from = { x: ctx.world.x, y: ctx.world.y };\n let last: SymmetryChange | null = null;\n const back: SymmetryChange = { spec: origin.spec, center: origin.center };\n return {\n kind: 'symmetry-guide',\n onMove(move: InvocationCtx): void {\n last = handleChange(press, origin, from, move.world, move.modifiers.shift, snap);\n ctrl.preview(id, last);\n },\n onEnd(_end: InvocationCtx, reason: 'commit' | 'cancel'): void {\n ctrl.preview(id, back);\n if (reason === 'commit' && last) ctrl.update(id, last, 'Move symmetry guide');\n },\n };\n },\n },\n };\n}\n","import { hitPredicate, type Action, type NodeId, type Scene, type SurfaceContribution } from '@weasel-js/core';\nimport { bakeSymmetry, type BakeResult } from './bake';\nimport {\n editSymmetries, isSymmetryGroup, poseAtCenter, readGroup, specOfData, symmetryGroupOf,\n symmetryGroupSpec,\n} from './group';\nimport { handleDragAction, SYMMETRY_HANDLE_ACTION_ID } from './handleDrag';\nimport { SYMMETRY_HANDLE_KIND, symmetryGuideLayer, type SymmetryGuideStyle } from './guide';\nimport type { SymmetrySpec } from './kinds';\n\ntype AnyScene = Scene<unknown, string, unknown>;\ntype Pt = { x: number; y: number };\n\n/** What changes on a group: its spec, its center, or both. */\nexport interface SymmetryChange {\n spec?: Partial<SymmetrySpec>;\n center?: Pt;\n}\n\n/** Live symmetry on one scene: which group is being drawn into, the edits a\n * group takes, and the canvas entry that wires it all up. */\nexport interface SymmetryController {\n readonly scene: AnyScene;\n /** The group new shapes are drawn into, or null for none. Answers null\n * once that group is gone, an undone create say. */\n active(): NodeId | null;\n setActive(id: NodeId | null): void;\n /** Hear that the active group, or anything about it, changed. */\n subscribe(listener: () => void): () => void;\n /** Add an empty group, one undo step, and make it the active one. Its layer\n * defaults to the scene's first. */\n create(opts: { center: Pt; symmetry?: Partial<SymmetrySpec>; layer?: string; parent?: NodeId | null }): NodeId;\n /** Group `id`'s spec and center, or null when it is not a symmetry group. */\n read(id: NodeId | null): { spec: SymmetrySpec; center: Pt } | null;\n /** Change a group, one undo step. */\n update(id: NodeId, change: SymmetryChange, label?: string): void;\n /** Change a group with no undo step, for the frames of a drag. */\n preview(id: NodeId, change: SymmetryChange): void;\n /** Replace a group with a plain one holding real copies. See {@link bakeSymmetry}. */\n bake(id: NodeId): BakeResult | null;\n /**\n * The canvas entry, for `<SceneCanvas ambient>`: it draws the active\n * group's guide with handles that drag, puts drawn shapes into the active\n * group (the `insertInto` dep), and keeps a path that is its own image\n * under its group symmetric while its anchors are edited (the\n * `pathSymmetry` dep). Built once per controller.\n */\n readonly contribution: SurfaceContribution;\n}\n\n/** Options for {@link createSymmetry}. */\nexport interface CreateSymmetryOptions {\n guide?: SymmetryGuideStyle;\n}\n\nconst GRAB_HANDLE_ID = 'symmetry.grabHandle';\nconst grabHandle: Action = {\n id: GRAB_HANDLE_ID,\n label: 'Grab symmetry handle',\n invoker: { timing: 'immediate', run: () => {} },\n enabled: () => true as const,\n};\n\nconst MODS_ANY = { shift: 'optional', alt: 'optional', meta: 'optional', ctrl: 'optional' } as const;\nconst onHandle = hitPredicate((hit) => (hit as { kind?: string } | null | undefined)?.kind === SYMMETRY_HANDLE_KIND);\n\n/** Live symmetry for `scene`. Pass `symmetryRegistry` to the scene too, so its groups save. */\nexport function createSymmetry(scene: AnyScene, opts: CreateSymmetryOptions = {}): SymmetryController {\n let activeId: NodeId | null = null;\n const listeners = new Set<() => void>();\n const notify = () => { for (const l of [...listeners]) l(); };\n\n const write = (id: NodeId, change: SymmetryChange): void => {\n const node = scene.get(id);\n if (!node || !isSymmetryGroup(scene, id)) return;\n if (change.spec) scene.update(id, { data: { ...(node.data as object), symmetry: { ...specOfData(node.data), ...change.spec } } });\n if (change.center) scene.setPose(id, { ...(node.pose as object), ...poseAtCenter(change.center) });\n };\n\n const ctrl: SymmetryController = {\n scene,\n active: () => (isSymmetryGroup(scene, activeId) ? activeId : null),\n setActive(id) {\n activeId = id;\n notify();\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => { listeners.delete(listener); };\n },\n create({ center, symmetry, layer, parent }) {\n const id = scene.add(symmetryGroupSpec({\n layer: layer ?? scene.layers[0]?.id ?? 'default', center, ...(symmetry ? { symmetry } : {}),\n ...(parent !== undefined ? { parent } : {}),\n }) as never);\n ctrl.setActive(id);\n return id;\n },\n read: (id) => readGroup(scene, id),\n update(id, change, label = 'Change symmetry') {\n scene.batch(label, () => write(id, change));\n notify();\n },\n preview(id, change) {\n scene.untracked(() => write(id, change));\n notify();\n },\n bake(id) {\n const out = bakeSymmetry(scene, id);\n notify();\n return out;\n },\n contribution: undefined as unknown as SurfaceContribution,\n };\n\n const guide = symmetryGuideLayer({\n current() {\n const id = ctrl.active();\n const g = readGroup(scene, id);\n return g === null || id === null ? null : { group: id, ...g };\n },\n subscribe: ctrl.subscribe,\n }, opts.guide);\n\n const action = handleDragAction(ctrl);\n (ctrl as { contribution: SurfaceContribution }).contribution = {\n id: 'weasel-symmetry',\n eligibility: { claimed: true },\n deps: {\n insertInto: () => ({ get: () => ctrl.active() }),\n pathSymmetry: () => ({ transforms: (pathId: string) => editSymmetries(scene, symmetryGroupOf(scene, pathId)) }),\n },\n actions: [action, grabHandle],\n bindings: [\n { spec: { kind: 'drag', target: { kindOf: onHandle }, mods: MODS_ANY }, actionId: SYMMETRY_HANDLE_ACTION_ID },\n // A handle's claim bars every other binding for the whole press, so its\n // pointer-down and a click that never becomes a drag need one too.\n { spec: { kind: 'pointerDown', target: `affordance:${SYMMETRY_HANDLE_KIND}` }, actionId: GRAB_HANDLE_ID },\n { spec: { kind: 'click', target: `affordance:${SYMMETRY_HANDLE_KIND}` }, actionId: GRAB_HANDLE_ID },\n ],\n attach: (api) => {\n const off = api.registerLayer(guide);\n api.requestRedraw();\n return off;\n },\n };\n return ctrl;\n}\n\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weasel-js/symmetry",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.10.1",
|
|
4
|
+
"description": "Live symmetry groups for weasel: a container whose mirrored, rotated and repeated copies are drawn from its real children, so every edit reaches every copy. No React.",
|
|
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
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"author": "orochi235",
|
|
19
|
+
"homepage": "https://orochi235.github.io/weasel/",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/orochi235/weasel.git",
|
|
23
|
+
"directory": "packages/symmetry"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/orochi235/weasel/issues"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@weasel-js/core": "1.10.1",
|
|
41
|
+
"@weasel-js/geom": "1.10.1"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public",
|
|
45
|
+
"provenance": true
|
|
46
|
+
}
|
|
47
|
+
}
|