rig-c 0.0.0-stage → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +13 -0
- package/LICENSE +30 -0
- package/NOTICE.md +145 -0
- package/README.md +817 -3
- package/bin/rigc.cjs +83 -0
- package/cli.ts +61 -0
- package/cli_core.ts +46 -0
- package/docs/AUTHORING.md +9923 -0
- package/docs/FACE.md +1948 -0
- package/docs/INGEST.md +1488 -0
- package/docs/MOTION.md +1241 -0
- package/docs/PROMPTING.md +109 -0
- package/docs/RIGGING.md +1441 -0
- package/docs/SPEC_COVERAGE.md +357 -0
- package/package.json +108 -4
- package/skills/rigc/SKILL.md +133 -0
- package/skills/rigc-face/SKILL.md +60 -0
- package/skills/rigc-ingest/SKILL.md +78 -0
- package/skills/rigc-motion/SKILL.md +51 -0
- package/skills/rigc-rigging/SKILL.md +49 -0
- package/src/areaband.ts +159 -0
- package/src/assertions/bodies/a01.ts +23 -0
- package/src/assertions/bodies/a02.ts +21 -0
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a04.ts +40 -0
- package/src/assertions/bodies/a05.ts +56 -0
- package/src/assertions/bodies/a06.ts +245 -0
- package/src/assertions/bodies/a07.ts +68 -0
- package/src/assertions/bodies/a08.ts +76 -0
- package/src/assertions/bodies/a09.ts +82 -0
- package/src/assertions/bodies/a10.ts +116 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a12.ts +30 -0
- package/src/assertions/bodies/a13.ts +51 -0
- package/src/assertions/bodies/a14.ts +35 -0
- package/src/assertions/bodies/a15.ts +97 -0
- package/src/assertions/bodies/a16.ts +24 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a18.ts +62 -0
- package/src/assertions/bodies/a19.ts +404 -0
- package/src/assertions/bodies/a20.ts +122 -0
- package/src/assertions/bodies/a21.ts +190 -0
- package/src/assertions/bodies/a22.ts +39 -0
- package/src/assertions/bodies/a23.ts +305 -0
- package/src/assertions/bodies/a24.ts +68 -0
- package/src/assertions/bodies/a25.ts +39 -0
- package/src/assertions/bodies/a26.ts +61 -0
- package/src/assertions/bodies/a27.ts +33 -0
- package/src/assertions/bodies/a28.ts +70 -0
- package/src/assertions/bodies/a29.ts +34 -0
- package/src/assertions/bodies/a30.ts +50 -0
- package/src/assertions/bodies/a31.ts +61 -0
- package/src/assertions/bodies/a32.ts +44 -0
- package/src/assertions/bodies/a33.ts +110 -0
- package/src/assertions/bodies/a34.ts +133 -0
- package/src/assertions/bodies/a35.ts +160 -0
- package/src/assertions/bodies/a36.ts +81 -0
- package/src/assertions/bodies/a37.ts +77 -0
- package/src/assertions/bodies/a38.ts +73 -0
- package/src/assertions/bodies/a39.ts +303 -0
- package/src/assertions/bodies/a40.ts +128 -0
- package/src/assertions/bodies/a42.ts +97 -0
- package/src/assertions/bodies/a43.ts +181 -0
- package/src/assertions/bodies/a44.ts +23 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/bodies/a46.ts +224 -0
- package/src/assertions/bodies/a47.ts +126 -0
- package/src/assertions/bodies/a48.ts +83 -0
- package/src/assertions/bodies/a49.ts +81 -0
- package/src/assertions/bodies/a50.ts +97 -0
- package/src/assertions/constraint_words.ts +169 -0
- package/src/assertions/emitted/index.ts +148 -0
- package/src/assertions/facts/animated_bones.ts +30 -0
- package/src/assertions/facts/animation_durations.ts +37 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/atlas_regions.ts +52 -0
- package/src/assertions/facts/bone_timelines.ts +37 -0
- package/src/assertions/facts/constraint_targets.ts +56 -0
- package/src/assertions/facts/constraints.ts +155 -0
- package/src/assertions/facts/deform_survey.ts +27 -0
- package/src/assertions/facts/event_keys.ts +55 -0
- package/src/assertions/facts/linked_meshes.ts +38 -0
- package/src/assertions/facts/mesh_attachments.ts +100 -0
- package/src/assertions/facts/region_joins.ts +34 -0
- package/src/assertions/facts/sequences.ts +85 -0
- package/src/assertions/facts/skeleton_roster.ts +45 -0
- package/src/assertions/facts/skin_entries.ts +37 -0
- package/src/assertions/facts/skin_members.ts +53 -0
- package/src/assertions/facts/slider_composition.ts +78 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/facts/stage.ts +27 -0
- package/src/assertions/facts/stage_box.ts +65 -0
- package/src/assertions/facts/stepped_poses.ts +74 -0
- package/src/assertions/facts/two_colour.ts +52 -0
- package/src/assertions/facts/vertex_polygons.ts +53 -0
- package/src/assertions/footprints.ts +367 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/inward_advance.ts +58 -0
- package/src/assertions/kinds.ts +105 -0
- package/src/assertions/mesh_kinds.ts +56 -0
- package/src/assertions/model/animated_bones.ts +38 -0
- package/src/assertions/model/animation_durations.ts +57 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/atlas_regions.ts +76 -0
- package/src/assertions/model/bone_timelines.ts +58 -0
- package/src/assertions/model/constraint_targets.ts +82 -0
- package/src/assertions/model/constraints.ts +233 -0
- package/src/assertions/model/declared.ts +125 -0
- package/src/assertions/model/deform_survey.ts +24 -0
- package/src/assertions/model/event_keys.ts +45 -0
- package/src/assertions/model/given.ts +45 -0
- package/src/assertions/model/index.ts +398 -0
- package/src/assertions/model/linked_meshes.ts +24 -0
- package/src/assertions/model/mesh_attachments.ts +119 -0
- package/src/assertions/model/parse.ts +146 -0
- package/src/assertions/model/region_joins.ts +67 -0
- package/src/assertions/model/runtime_timelines.ts +78 -0
- package/src/assertions/model/sequences.ts +157 -0
- package/src/assertions/model/skeleton_roster.ts +23 -0
- package/src/assertions/model/skin_entries.ts +69 -0
- package/src/assertions/model/skin_members.ts +64 -0
- package/src/assertions/model/slider_composition.ts +193 -0
- package/src/assertions/model/slot_colour.ts +81 -0
- package/src/assertions/model/stage.ts +28 -0
- package/src/assertions/model/stage_box.ts +51 -0
- package/src/assertions/model/stepped_poses.ts +105 -0
- package/src/assertions/model/two_colour.ts +61 -0
- package/src/assertions/model/vertex_polygons.ts +72 -0
- package/src/assertions/reasons.ts +129 -0
- package/src/assertions/region_lookups.ts +61 -0
- package/src/assertions/report.ts +189 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +2870 -0
- package/src/ballot.ts +866 -0
- package/src/bonedist.ts +643 -0
- package/src/chainfit.ts +2752 -0
- package/src/chains.ts +170 -0
- package/src/check.ts +4303 -0
- package/src/checkpics.ts +295 -0
- package/src/cli/core_commands.ts +1627 -0
- package/src/cli/repack.ts +414 -0
- package/src/cli/shared.ts +2776 -0
- package/src/cli/spine_commands.ts +820 -0
- package/src/compile.ts +9414 -0
- package/src/core/additive.ts +458 -0
- package/src/core/animation.ts +1050 -0
- package/src/core/clipping.ts +696 -0
- package/src/core/constraints.ts +1876 -0
- package/src/core/constraints_path.ts +964 -0
- package/src/core/constraints_physics.ts +881 -0
- package/src/core/constraints_slider.ts +635 -0
- package/src/core/deform.ts +613 -0
- package/src/core/draw_order.ts +125 -0
- package/src/core/events.ts +135 -0
- package/src/core/hooks.ts +249 -0
- package/src/core/index.ts +1400 -0
- package/src/core/raw.ts +739 -0
- package/src/core/skins.ts +129 -0
- package/src/core/uvs.ts +469 -0
- package/src/core/vertices.ts +490 -0
- package/src/core/walk.ts +197 -0
- package/src/core/world.ts +289 -0
- package/src/correspondence.ts +15 -0
- package/src/deformbuild.ts +60 -0
- package/src/deformgen.ts +630 -0
- package/src/deformmeasure.ts +732 -0
- package/src/deformreport.ts +373 -0
- package/src/deformstructure.ts +386 -0
- package/src/deformsurvey.ts +2162 -0
- package/src/depth.ts +784 -0
- package/src/diff.ts +2252 -0
- package/src/emit.ts +134 -0
- package/src/emit_spine.ts +854 -0
- package/src/errors.ts +53 -0
- package/src/framing.ts +819 -0
- package/src/generation.ts +139 -0
- package/src/ingest.ts +2293 -0
- package/src/json-position.ts +253 -0
- package/src/keyorder.ts +587 -0
- package/src/keys.ts +486 -0
- package/src/ladder.ts +121 -0
- package/src/mesh.ts +2382 -0
- package/src/meshcompare.ts +1191 -0
- package/src/meshquality.ts +2051 -0
- package/src/meshrasters.ts +944 -0
- package/src/meshreduce.ts +1444 -0
- package/src/model.ts +1245 -0
- package/src/motion.ts +809 -0
- package/src/nonfinite.ts +54 -0
- package/src/package_meta.ts +48 -0
- package/src/png.ts +297 -0
- package/src/pose.ts +2324 -0
- package/src/preview.ts +434 -0
- package/src/region_joins.ts +54 -0
- package/src/render.ts +1013 -0
- package/src/render_core.ts +871 -0
- package/src/render_shared.ts +2958 -0
- package/src/repack.ts +495 -0
- package/src/rig.ts +2941 -0
- package/src/slots.ts +892 -0
- package/src/spine_side.ts +138 -0
- package/src/timelines.ts +837 -0
- package/src/trackgen.ts +364 -0
- package/src/transform.ts +310 -0
- package/src/types.ts +1797 -0
- package/src/validate.ts +3875 -0
- package/tools/contact.ts +126 -0
- package/tools/editor_roundtrip.ts +1641 -0
- package/tools/font5x7.ts +101 -0
- package/tools/measure_contact_depth.ts +105 -0
- package/tools/plate.ts +508 -0
- package/tools/png_probe.mjs +72 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The core's sixth construct: clipping applied to the draw (issue #964, §5
|
|
3
|
+
* item 6 of the design on issue #380) — the oracle's `clipped` block, the
|
|
4
|
+
* triangles every region and mesh drawn while a clip is active hands to the
|
|
5
|
+
* runtime's clipper, as the clipper returns them.
|
|
6
|
+
*
|
|
7
|
+
* ## How every rule below was fixed
|
|
8
|
+
*
|
|
9
|
+
* By running spine-core 4.3.13's `SkeletonClipping` — `clipStart`,
|
|
10
|
+
* `clipEnd(slot)`, `clipEnd()`, `isClipping` and `clipTrianglesUnpacked`, the
|
|
11
|
+
* calls `src/render.ts` makes, in the order it makes them — on hand-written
|
|
12
|
+
* skeletons and on random triangles and polygons, and reading what it
|
|
13
|
+
* returned: the return value, `clippedVerticesTyped`, `clippedUVsTyped`,
|
|
14
|
+
* `clippedTrianglesTyped`. The runtime's source was not read. Each rule is a
|
|
15
|
+
* reading that reproduced every case measured, next to the readings the
|
|
16
|
+
* measurements rejected. The core suite's `CL` controls hold a hand-written
|
|
17
|
+
* skeleton of every case against the runtime at tolerance 0.
|
|
18
|
+
*
|
|
19
|
+
* ## When a clip is active — the draw walk
|
|
20
|
+
*
|
|
21
|
+
* The walk is `src/render.ts`'s `piecesOf`: slots in the posed draw order; at
|
|
22
|
+
* a clipping attachment, end the clip if this slot is its end, then start
|
|
23
|
+
* this clip if its slot's bone is active and no clip is active; at every
|
|
24
|
+
* other slot, clip what it draws while a clip is active, then end the clip
|
|
25
|
+
* if this slot is its end. Measured on one hand-written skeleton per case:
|
|
26
|
+
*
|
|
27
|
+
* | case | read |
|
|
28
|
+
* | --- | --- |
|
|
29
|
+
* | a clip ending at a slot | that slot is clipped, the one after it is not |
|
|
30
|
+
* | ending at a slot that shows nothing | ends there all the same |
|
|
31
|
+
* | ending at a slot drawn BEFORE the clip, or at its own slot | never ends: every later slot is clipped |
|
|
32
|
+
* | no end slot | every later slot is clipped |
|
|
33
|
+
* | a second clip while the first is active | ignored: a region inside the second and outside the first read wholly cut |
|
|
34
|
+
* | a second clip after the first ended | applies |
|
|
35
|
+
* | a clip ending at the slot of the next clip | the next clip starts there |
|
|
36
|
+
* | a clip on an inactive bone | starts nothing |
|
|
37
|
+
* | a region on an inactive bone under a clip | clipped like any other |
|
|
38
|
+
*
|
|
39
|
+
* ## What one triangle becomes
|
|
40
|
+
*
|
|
41
|
+
* The clip polygon is the clipping attachment's world polygon (the `clips`
|
|
42
|
+
* row, `./vertices.ts`), unrounded. When its signed area is positive (counter
|
|
43
|
+
* clockwise, y up) its vertices are reversed, so it is walked clockwise, and
|
|
44
|
+
* its edges are taken from vertex 0: `(v0, v1), (v1, v2), …, (vn-1, v0)`.
|
|
45
|
+
* A point is inside an edge `(p, q)` when `(q.x − p.x)(y − p.y) − (q.y −
|
|
46
|
+
* p.y)(x − p.x) < 0`, strictly.
|
|
47
|
+
*
|
|
48
|
+
* Each triangle `(a, b, c)` of the attachment, in its own order, is clipped
|
|
49
|
+
* edge by edge (Sutherland–Hodgman): walking the current polygon's sides
|
|
50
|
+
* `(s, e)` from `(v0, v1)` round to `(vk, v0)`, an inside `s` with an inside
|
|
51
|
+
* `e` keeps `e`; inside to outside adds the intersection; outside to inside
|
|
52
|
+
* adds the intersection and then `e`; outside to outside adds nothing. The
|
|
53
|
+
* intersection is `s + t·(e − s)` with `t = ((q.x − p.x)(s.y − p.y) − (q.y −
|
|
54
|
+
* p.y)(s.x − p.x)) / ((q.y − p.y)(e.x − s.x) − (q.x − p.x)(e.y − s.y))`, in
|
|
55
|
+
* doubles. A triangle no side of which was outside any edge is returned as it
|
|
56
|
+
* came, `a, b, c` — not the rotation the walk leaves it in. A result of fewer
|
|
57
|
+
* than three points adds nothing; otherwise its points are appended and fanned
|
|
58
|
+
* from the first: `(0, 1, 2), (0, 2, 3), …` offset by the vertices already
|
|
59
|
+
* written. The clipper stores vertices and UVs as float32: every number
|
|
60
|
+
* written goes through `Math.fround`.
|
|
61
|
+
*
|
|
62
|
+
* A triangle no side of which was outside any edge keeps its corners' own
|
|
63
|
+
* UVs (through `Math.fround`) rather than weighing them — the two agree on
|
|
64
|
+
* the grid, and on three rows of `spineboy-pro`'s portal the weighed UV of a
|
|
65
|
+
* corner at 0 read `2e-17` where the runtime holds 0 (issue #966, `dump
|
|
66
|
+
* --raw`). Each point of a triangle that was cut has a UV barycentric over
|
|
67
|
+
* the source triangle, from the unrounded
|
|
68
|
+
* point: with `d = 1 / ((b.y − c.y)(a.x − c.x) + (c.x − b.x)(a.y − c.y))`,
|
|
69
|
+
* `wa = ((b.y − c.y)(x − c.x) + (c.x − b.x)(y − c.y))·d`,
|
|
70
|
+
* `wb = ((c.y − a.y)(x − c.x) + (a.x − c.x)(y − c.y))·d`, `wc = 1 − wa − wb`,
|
|
71
|
+
* and `uv = ua·wa + ub·wb + uc·wc`, the corner UVs read through
|
|
72
|
+
* `Math.fround`.
|
|
73
|
+
*
|
|
74
|
+
* The clipper returns `true` when any triangle had a point outside any edge,
|
|
75
|
+
* else `false` — and `src/render.ts` then draws the attachment's own geometry.
|
|
76
|
+
*
|
|
77
|
+
* Measured on 2641 random cases, every polygon strictly convex, both
|
|
78
|
+
* windings, one to three triangles of both windings each: 400 against
|
|
79
|
+
* polygons of three to seven vertices, 600 whose intersections fall near the
|
|
80
|
+
* origin, 1200 with corner UVs chosen so the weights' rounding shows, 300
|
|
81
|
+
* cutting one edge once, and 141 random star polygons that came out convex —
|
|
82
|
+
* the vertex list, the UVs, the triangle list and the return value exact on
|
|
83
|
+
* 2641 of 2641. On the public corpus, `spineboy-pro`'s portal animation
|
|
84
|
+
* (a three-vertex clip ending at a slot) reads IDENTICAL at tolerance 0 on
|
|
85
|
+
* 4338 rows drawn under it (241 dense samples; 2653 left whole, 106 cut,
|
|
86
|
+
* 1579 cut away). Rejected readings, each with what it read:
|
|
87
|
+
*
|
|
88
|
+
* | reading | read |
|
|
89
|
+
* | --- | --- |
|
|
90
|
+
* | the polygon's own winding, edges from vertex 0 | triangle lists 219 of 400; vertices 36 |
|
|
91
|
+
* | the polygon made counter-clockwise, inside on the left | triangle lists 400; vertices in order 130 of 400 |
|
|
92
|
+
* | a triangle wholly inside returned as the walk leaves it | 396 of 400 in order — the four wholly inside rotated |
|
|
93
|
+
* | the polygon as its JSON spells it, through `Math.fround` | 79 of 400 exact: the world polygon carries the bone's matrix (`b` is `cos 90°` at the runtime's pi, −2.3e-8) |
|
|
94
|
+
* | the intersection from the clip edge, `p + u·(q − p)` | 228 of 300 on a horizontal edge at y = 0, where `s + t·(e − s)` read 300 |
|
|
95
|
+
* | the UV weights by a division rather than a reciprocal | 586 of 600 |
|
|
96
|
+
* | the UV weights relative to `a` (`wb, wc` solved, `wa = 1 − wb − wc`) | 105 of 600 |
|
|
97
|
+
*
|
|
98
|
+
* ## What is not posed, and why
|
|
99
|
+
*
|
|
100
|
+
* The block is left out, naming the slot, when a clip that starts is one of:
|
|
101
|
+
*
|
|
102
|
+
* - ⚠️ **not strictly convex** — a reflex vertex, three collinear vertices or
|
|
103
|
+
* a repeated one. The runtime decomposes such a polygon into convex pieces
|
|
104
|
+
* and clips each triangle against each piece in turn: a covering triangle
|
|
105
|
+
* read the pieces (a notched square into three triangles, an L into two
|
|
106
|
+
* quads; a collinear vertex dropped; a repeated vertex clipped everything
|
|
107
|
+
* away). Which pieces, in which order and from which vertex is the
|
|
108
|
+
* runtime's own decomposition, and the dump alone did not fix it (issue
|
|
109
|
+
* #964's report). Posing it would be guessing an algorithm.
|
|
110
|
+
* - **`inverse`** — the clip keeps what is outside the polygon, through a
|
|
111
|
+
* decomposition of the triangle around it (a covering triangle came back as
|
|
112
|
+
* fifteen vertices in five fans). Not measured past that.
|
|
113
|
+
*
|
|
114
|
+
* What the core DRAWS under such a clip — a decomposition of its own, the
|
|
115
|
+
* `clipped` block still left out — is the last section of this file
|
|
116
|
+
* (`clipThrough`, issue #964).
|
|
117
|
+
*
|
|
118
|
+
* `convex: true` on a strictly convex polygon changes nothing: 400 of 400
|
|
119
|
+
* cases byte-identical with and without it. On a polygon that is not, the
|
|
120
|
+
* runtime clips against its convex hull (a notched square with `convex` read
|
|
121
|
+
* as the square); that is the first case above and is left out.
|
|
122
|
+
*/
|
|
123
|
+
/** One row of `clipped`: `[slot, attachment, clipped, vertices, uvs, triangles]`, the oracle's row. */
|
|
124
|
+
export type CoreClippedRow = [string, string, 0 | 1, Array<number | null>, Array<number | null>, number[]];
|
|
125
|
+
|
|
126
|
+
/** What the clipper returns for one attachment: the return value and the three arrays, unrounded to the oracle's grid. */
|
|
127
|
+
export interface ClipResult {
|
|
128
|
+
clipped: boolean;
|
|
129
|
+
vertices: number[];
|
|
130
|
+
uvs: number[];
|
|
131
|
+
triangles: number[];
|
|
132
|
+
/** The source triangle (an index into the attachment's triangles by threes) each returned triangle was cut from — what the render samples it at (issue #964). */
|
|
133
|
+
sources: number[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
type Pt = [number, number];
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The readings the header's table rejected, each a switch — what the core
|
|
140
|
+
* suite's `CL` plants pass to show the gate names it. `clipTriangles` with
|
|
141
|
+
* none set is the rule measured; nothing but a plant sets one.
|
|
142
|
+
*/
|
|
143
|
+
export interface ClipReading {
|
|
144
|
+
/** Walk the polygon in its own winding, not clockwise. */
|
|
145
|
+
ownWinding?: boolean;
|
|
146
|
+
/** Walk the polygon counter-clockwise — the runtime's winding flipped. */
|
|
147
|
+
flippedWinding?: boolean;
|
|
148
|
+
/** Return a triangle wholly inside as the walk leaves it. */
|
|
149
|
+
insideRotated?: boolean;
|
|
150
|
+
/** Take the intersection from the clip edge, `p + u·(q − p)`. */
|
|
151
|
+
fromClipEdge?: boolean;
|
|
152
|
+
/** Divide the UV weights by the determinant rather than multiply by its reciprocal. */
|
|
153
|
+
uvByDivision?: boolean;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** A world polygon's signed area, doubled: positive when counter-clockwise (y up). */
|
|
157
|
+
export function signedArea2(polygon: readonly number[]): number {
|
|
158
|
+
let a = 0;
|
|
159
|
+
const n = polygon.length / 2;
|
|
160
|
+
for (let i = 0; i < n; i++) {
|
|
161
|
+
const j = (i + 1) % n;
|
|
162
|
+
a += polygon[2 * i] * polygon[2 * j + 1] - polygon[2 * j] * polygon[2 * i + 1];
|
|
163
|
+
}
|
|
164
|
+
return a;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Why a world polygon is not one the core clips against — not strictly convex — or null. */
|
|
168
|
+
export function convexWhy(polygon: readonly number[]): string | null {
|
|
169
|
+
const n = polygon.length / 2;
|
|
170
|
+
if (n < 3) return `it has ${n} vertex(es)`;
|
|
171
|
+
let sign = 0;
|
|
172
|
+
for (let i = 0; i < n; i++) {
|
|
173
|
+
const [ax, ay] = [polygon[2 * i], polygon[2 * i + 1]];
|
|
174
|
+
const j = (i + 1) % n;
|
|
175
|
+
const k = (i + 2) % n;
|
|
176
|
+
const cross = (polygon[2 * j] - ax) * (polygon[2 * k + 1] - polygon[2 * j + 1]) - (polygon[2 * j + 1] - ay) * (polygon[2 * k] - polygon[2 * j]);
|
|
177
|
+
if (cross === 0) return `vertices ${i}, ${j} and ${k} are collinear or repeated`;
|
|
178
|
+
const s = Math.sign(cross);
|
|
179
|
+
if (sign === 0) sign = s;
|
|
180
|
+
else if (s !== sign) return `vertex ${j} turns against the others (a reflex vertex)`;
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The polygon walked clockwise, as the clipper walks it (the header). */
|
|
186
|
+
function clockwise(polygon: readonly number[], reading: ClipReading): Pt[] {
|
|
187
|
+
const pts: Pt[] = [];
|
|
188
|
+
for (let i = 0; i + 1 < polygon.length; i += 2) pts.push([polygon[i], polygon[i + 1]]);
|
|
189
|
+
if (reading.ownWinding === true) return pts;
|
|
190
|
+
const cw = signedArea2(polygon) > 0 ? pts.reverse() : pts;
|
|
191
|
+
return reading.flippedWinding === true ? cw.reverse() : cw;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function intersectFromClipEdge(s: Pt, e: Pt, p: Pt, q: Pt): Pt {
|
|
195
|
+
const dx = e[0] - s[0];
|
|
196
|
+
const dy = e[1] - s[1];
|
|
197
|
+
const ex = q[0] - p[0];
|
|
198
|
+
const ey = q[1] - p[1];
|
|
199
|
+
const u = (dx * (s[1] - p[1]) - dy * (s[0] - p[0])) / (ey * dx - ex * dy);
|
|
200
|
+
return [p[0] + u * ex, p[1] + u * ey];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function intersect(s: Pt, e: Pt, p: Pt, q: Pt): Pt {
|
|
204
|
+
const dx = e[0] - s[0];
|
|
205
|
+
const dy = e[1] - s[1];
|
|
206
|
+
const ex = q[0] - p[0];
|
|
207
|
+
const ey = q[1] - p[1];
|
|
208
|
+
const t = (ex * (s[1] - p[1]) - ey * (s[0] - p[0])) / (ey * dx - ex * dy);
|
|
209
|
+
return [s[0] + t * dx, s[1] + t * dy];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** One triangle against the clockwise polygon: the points, and whether any side was outside any edge. */
|
|
213
|
+
function clipTriangle(tri: readonly Pt[], poly: readonly Pt[], reading: ClipReading): { points: Pt[]; cut: boolean } {
|
|
214
|
+
const cross = reading.fromClipEdge === true ? intersectFromClipEdge : intersect;
|
|
215
|
+
let input: Pt[] = [...tri];
|
|
216
|
+
let cut = false;
|
|
217
|
+
for (let i = 0; i < poly.length && input.length > 0; i++) {
|
|
218
|
+
const p = poly[i];
|
|
219
|
+
const q = poly[(i + 1) % poly.length];
|
|
220
|
+
const inside = (v: Pt): boolean => (q[0] - p[0]) * (v[1] - p[1]) - (q[1] - p[1]) * (v[0] - p[0]) < 0;
|
|
221
|
+
const out: Pt[] = [];
|
|
222
|
+
for (let k = 0; k < input.length; k++) {
|
|
223
|
+
const s = input[k];
|
|
224
|
+
const e = input[(k + 1) % input.length];
|
|
225
|
+
const si = inside(s);
|
|
226
|
+
const ei = inside(e);
|
|
227
|
+
if (si) {
|
|
228
|
+
if (ei) out.push(e);
|
|
229
|
+
else {
|
|
230
|
+
out.push(cross(s, e, p, q));
|
|
231
|
+
cut = true;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
cut = true;
|
|
235
|
+
if (ei) out.push(cross(s, e, p, q), e);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
input = out;
|
|
239
|
+
}
|
|
240
|
+
return { points: cut || reading.insideRotated === true ? input : [...tri], cut };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* An attachment's triangles clipped against a strictly convex world polygon —
|
|
245
|
+
* the header's rule. `vertices` are the attachment's world vertices, `uvs`
|
|
246
|
+
* one pair per vertex, `triangles` indices into them.
|
|
247
|
+
*/
|
|
248
|
+
export function clipTriangles(polygon: readonly number[], vertices: readonly number[], triangles: readonly number[], uvs: readonly number[], reading: ClipReading = {}): ClipResult {
|
|
249
|
+
const poly = clockwise(polygon, reading);
|
|
250
|
+
const f = Math.fround;
|
|
251
|
+
const outV: number[] = [];
|
|
252
|
+
const outUv: number[] = [];
|
|
253
|
+
const outT: number[] = [];
|
|
254
|
+
const sources: number[] = [];
|
|
255
|
+
let clipped = false;
|
|
256
|
+
for (let k = 0; k + 2 < triangles.length; k += 3) {
|
|
257
|
+
const idx = [triangles[k], triangles[k + 1], triangles[k + 2]];
|
|
258
|
+
const tri = idx.map((i): Pt => [vertices[2 * i], vertices[2 * i + 1]]);
|
|
259
|
+
const tuv = idx.map((i): Pt => [f(uvs[2 * i]), f(uvs[2 * i + 1])]);
|
|
260
|
+
const { points, cut } = clipTriangle(tri, poly, reading);
|
|
261
|
+
for (let j = 1; j + 1 < points.length; j++) sources.push(k / 3);
|
|
262
|
+
clipped ||= cut;
|
|
263
|
+
if (points.length < 3) continue;
|
|
264
|
+
const [a, b, c] = tri;
|
|
265
|
+
const d0 = b[1] - c[1];
|
|
266
|
+
const d1 = c[0] - b[0];
|
|
267
|
+
const d2 = a[0] - c[0];
|
|
268
|
+
const d4 = c[1] - a[1];
|
|
269
|
+
const det = d0 * d2 + d1 * (a[1] - c[1]);
|
|
270
|
+
const d = 1 / det;
|
|
271
|
+
const weigh = (v: number): number => (reading.uvByDivision === true ? v / det : v * d);
|
|
272
|
+
const base = outV.length / 2;
|
|
273
|
+
// A triangle no side of which was outside any edge keeps its own corner UVs (issue #966): weighed barycentrically, a corner's UV of 0 reads 2e-17 off it, 3 rows of `spineboy-pro`'s portal.
|
|
274
|
+
if (!cut && reading.insideRotated !== true) {
|
|
275
|
+
tri.forEach(([x, y], k2) => {
|
|
276
|
+
outV.push(f(x), f(y));
|
|
277
|
+
outUv.push(tuv[k2][0], tuv[k2][1]);
|
|
278
|
+
});
|
|
279
|
+
for (let j = 1; j + 1 < points.length; j++) outT.push(base, base + j, base + j + 1);
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
for (const [x, y] of points) {
|
|
283
|
+
const wa = weigh(d0 * (x - c[0]) + d1 * (y - c[1]));
|
|
284
|
+
const wb = weigh(d4 * (x - c[0]) + d2 * (y - c[1]));
|
|
285
|
+
const wc = 1 - wa - wb;
|
|
286
|
+
outV.push(f(x), f(y));
|
|
287
|
+
outUv.push(f(tuv[0][0] * wa + tuv[1][0] * wb + tuv[2][0] * wc), f(tuv[0][1] * wa + tuv[1][1] * wb + tuv[2][1] * wc));
|
|
288
|
+
}
|
|
289
|
+
for (let j = 1; j + 1 < points.length; j++) outT.push(base, base + j, base + j + 1);
|
|
290
|
+
}
|
|
291
|
+
return { clipped, vertices: outV, uvs: outUv, triangles: outT, sources };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The clipper's reading of one attachment, replaceable by the suite's plants. */
|
|
295
|
+
export type TriangleClipper = (polygon: readonly number[], vertices: readonly number[], triangles: readonly number[], uvs: readonly number[]) => ClipResult;
|
|
296
|
+
|
|
297
|
+
/** One step of the draw walk: a slot, what its bone's activity is, and what it shows as the walk reads it. */
|
|
298
|
+
export type DrawStep =
|
|
299
|
+
| { slot: string; kind: 'clip'; attachment: string; active: boolean; end: string | null; polygon: number[]; inverse: boolean; convex: boolean }
|
|
300
|
+
| { slot: string; kind: 'draw'; attachment: string; vertices: number[]; triangles: number[]; uvs: number[] }
|
|
301
|
+
| { slot: string; kind: 'none' };
|
|
302
|
+
|
|
303
|
+
/** The local UVs the oracle hands a region's four corners to the clipper with, in the corner order (bottom-left, upper-left, upper-right, bottom-right). */
|
|
304
|
+
export const REGION_UVS: readonly number[] = [0, 1, 0, 0, 1, 0, 1, 1];
|
|
305
|
+
/** A region's two triangles over its four corners — the runtime's quad triangulation (`src/render.ts`'s `QUAD_TRIANGLES`). */
|
|
306
|
+
export const REGION_TRIANGLES: readonly number[] = [0, 1, 2, 2, 3, 0];
|
|
307
|
+
|
|
308
|
+
/** `clipThrough`'s shape, replaceable by the render suite's plants (`RC08`'s dropped region). */
|
|
309
|
+
export type ShapeClipper = (shape: ClipShape, vertices: readonly number[], triangles: readonly number[], uvs: readonly number[]) => ClipResult | null;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The `clipped` block from the draw walk (the header's first table), every
|
|
313
|
+
* number through `round`, or why it is absent: a clip that starts over a
|
|
314
|
+
* polygon the core does not reproduce the runtime's triangle list for (not
|
|
315
|
+
* strictly convex, or inverse), each named.
|
|
316
|
+
*
|
|
317
|
+
* Beside it, `drawn`: the same walk's rows as the core DRAWS them — the block's
|
|
318
|
+
* own rows wherever the block is posed, and, where a clip that is not strictly
|
|
319
|
+
* convex or is inverse starts, that clip cut through `clipThrough` (a
|
|
320
|
+
* decomposition of the core's own, held to the runtime by the render's pixels,
|
|
321
|
+
* issue #964). `drawnWhy` names a clip the core does not draw at all (a
|
|
322
|
+
* polygon that is not simple).
|
|
323
|
+
*/
|
|
324
|
+
export function poseClipped(
|
|
325
|
+
walk: readonly DrawStep[],
|
|
326
|
+
round: (v: number) => number | null,
|
|
327
|
+
clip: TriangleClipper = clipTriangles,
|
|
328
|
+
through: ShapeClipper = (shape, v, t, uv) => clipThrough(shape, v, t, uv, clip),
|
|
329
|
+
): { rows: CoreClippedRow[] | null; why: string | null; drawn: CoreClippedRow[] | null; drawnWhy: string | null } {
|
|
330
|
+
const rows: CoreClippedRow[] = [];
|
|
331
|
+
const refused: string[] = [];
|
|
332
|
+
const undrawn: string[] = [];
|
|
333
|
+
let active: { end: string | null; shape: ClipShape } | null = null;
|
|
334
|
+
for (const step of walk) {
|
|
335
|
+
if (step.kind === 'clip') {
|
|
336
|
+
if (active !== null && active.end === step.slot) active = null;
|
|
337
|
+
if (step.active && active === null) {
|
|
338
|
+
const why = step.inverse ? 'it is inverse' : convexWhy(step.polygon);
|
|
339
|
+
if (why !== null) refused.push(`slot "${step.slot}" starts clip "${step.attachment}", and ${why}`);
|
|
340
|
+
const shape: ClipShape = { polygon: step.polygon, inverse: step.inverse, convex: step.convex };
|
|
341
|
+
const plan = clipShapeOf(shape);
|
|
342
|
+
if (plan.kind === 'refused') undrawn.push(`slot "${step.slot}" starts clip "${step.attachment}", and ${plan.why}`);
|
|
343
|
+
active = { end: step.end, shape };
|
|
344
|
+
}
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (step.kind === 'draw' && active !== null) {
|
|
348
|
+
const r = through(active.shape, step.vertices, step.triangles, step.uvs);
|
|
349
|
+
if (r !== null) rows.push([step.slot, step.attachment, r.clipped ? 1 : 0, r.vertices.map(round), r.uvs.map(round), r.triangles]);
|
|
350
|
+
}
|
|
351
|
+
if (active !== null && active.end === step.slot) active = null;
|
|
352
|
+
}
|
|
353
|
+
const drawnWhy = undrawn.length === 0
|
|
354
|
+
? null
|
|
355
|
+
: `${undrawn.join('; ')} — the runtime's coverage of a polygon that is not simple is its own triangulation's, measured not to be the polygon's area (src/core/clipping.ts)`;
|
|
356
|
+
const drawn = drawnWhy === null ? rows : null;
|
|
357
|
+
if (refused.length > 0) {
|
|
358
|
+
return { rows: null, why: `${refused.join('; ')} — the runtime decomposes such a polygon into convex pieces of its own choosing, or clips the outside of an inverse one, and neither was reproduced from the dump (src/core/clipping.ts)`, drawn, drawnWhy };
|
|
359
|
+
}
|
|
360
|
+
return { rows, why: null, drawn, drawnWhy };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---------------------------------------------------------------------------
|
|
364
|
+
// #964 what the core draws under a concave or inverse clip
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
//
|
|
367
|
+
// The oracle's `clipped` block under such a clip is the runtime's own triangle list and stays
|
|
368
|
+
// absent by name (above). What the core DRAWS there is a decomposition of its own, and the claim
|
|
369
|
+
// it makes is coverage, not the list: the union of the pieces is the polygon whatever the pieces
|
|
370
|
+
// are. Every reading below was fixed by running spine-core 4.3.13's `SkeletonClipping` (the calls
|
|
371
|
+
// `src/render.ts` makes) and reading the area it returned; the source was not read, and the
|
|
372
|
+
// runtime's decomposition was not reconstructed. The runtime's documentation of the two flags
|
|
373
|
+
// (`ClippingAttachment.convex` / `.inverse` in its public type declarations) says `convex` clips
|
|
374
|
+
// by the convex hull of a polygon that is not convex, and that inverse clipping is always convex.
|
|
375
|
+
//
|
|
376
|
+
// | polygon | the core | measured against spine-core |
|
|
377
|
+
// | --- | --- | --- |
|
|
378
|
+
// | strictly convex, not inverse | the measured rule above, one piece: the oracle's rows | the header's 2641 cases |
|
|
379
|
+
// | simple, a reflex or a collinear vertex | collinear vertices dropped, ear clipping from vertex 0, pieces merged while strictly convex (Hertel–Mehlhorn); each triangle clipped against each piece in turn by the rule above | area and return value equal on every case of `CL05` |
|
|
380
|
+
// | `convex: true`, simple | its convex hull, one piece | area equal (`CL05`); the hull is the documented reading |
|
|
381
|
+
// | `inverse: true`, simple | the triangle outside the hull: for each hull edge in the walk, the part outside it and inside every edge before it | area and return value equal (`CL05`): the return value is `true` for every triangle, cut or not, as the runtime's was |
|
|
382
|
+
// | an otherwise strictly convex polygon with a repeated vertex, not inverse | the rule above as it stands: its zero-length edge has no inside, so nothing is drawn | the runtime drew nothing on 141 of 141 random cases |
|
|
383
|
+
// | a repeated vertex anywhere else | **refused**, naming the vertices | the runtime drew nothing on 11 of 159 random non-convex cases and the polygon's area on the other 148 — which, is its decomposition's |
|
|
384
|
+
// | not simple (edges crossing or touching) | **refused**, naming the edges | a self-overlapping six-vertex fan read an area of 2.67 where its even-odd fill is 26.7, its non-zero fill 29.4 and its shoelace area 32; its `convex` and `inverse` readings used the same 2.67 |
|
|
385
|
+
//
|
|
386
|
+
// ⭐ The render draws through this pixel for pixel as spine-core draws (`RC08`), because the
|
|
387
|
+
// rasteriser samples each drawn triangle at its SOURCE triangle's affine UV (`Mesh.source` in
|
|
388
|
+
// `src/render.ts`, the source index being `ClipResult.sources`): before that, a pixel falling in
|
|
389
|
+
// a sub-triangle other than the runtime's was sampled at a UV a few float32 steps off, and one
|
|
390
|
+
// level of one channel moved on up to 50 pixels of a probe's frame set — as much as the runtime's
|
|
391
|
+
// own render moved when the same polygon was spelled from another start vertex.
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* A clip as the draw reads it: its world polygon and its two flags. Which
|
|
395
|
+
* reading `clipThrough` takes is `clipShapeOf`'s (the table in this section's
|
|
396
|
+
* header, `clipThrough`).
|
|
397
|
+
*/
|
|
398
|
+
export interface ClipShape {
|
|
399
|
+
polygon: readonly number[];
|
|
400
|
+
inverse: boolean;
|
|
401
|
+
convex: boolean;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* How a clip is drawn through the core: `convex` — the measured rule above,
|
|
406
|
+
* one piece, the oracle's `clipped` block reproduced to the bit; `pieces` —
|
|
407
|
+
* a simple polygon that is not strictly convex, cut into convex pieces of the
|
|
408
|
+
* core's own choosing; `inverse` — the outside of the polygon's convex hull;
|
|
409
|
+
* `empty` — an otherwise strictly convex polygon with a repeated vertex, which
|
|
410
|
+
* the measured rule itself clips everything away against. `refused` names why
|
|
411
|
+
* the core does not draw it.
|
|
412
|
+
*/
|
|
413
|
+
export type ClipPlan =
|
|
414
|
+
| { kind: 'convex'; polygon: readonly number[] }
|
|
415
|
+
| { kind: 'pieces'; pieces: Pt[][] }
|
|
416
|
+
| { kind: 'inverse'; hull: Pt[] }
|
|
417
|
+
| { kind: 'refused'; why: string };
|
|
418
|
+
|
|
419
|
+
function pointsOf(polygon: readonly number[]): Pt[] {
|
|
420
|
+
const pts: Pt[] = [];
|
|
421
|
+
for (let i = 0; i + 1 < polygon.length; i += 2) pts.push([polygon[i], polygon[i + 1]]);
|
|
422
|
+
return pts;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function cross3(o: Pt, a: Pt, b: Pt): number {
|
|
426
|
+
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Whether segments `ab` and `cd` meet, touching included (exact arithmetic on the doubles given). */
|
|
430
|
+
function segmentsMeet(a: Pt, b: Pt, c: Pt, d: Pt): boolean {
|
|
431
|
+
const d1 = cross3(c, d, a);
|
|
432
|
+
const d2 = cross3(c, d, b);
|
|
433
|
+
const d3 = cross3(a, b, c);
|
|
434
|
+
const d4 = cross3(a, b, d);
|
|
435
|
+
if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) return true;
|
|
436
|
+
const on = (p: Pt, q: Pt, r: Pt): boolean => Math.min(p[0], q[0]) <= r[0] && r[0] <= Math.max(p[0], q[0]) && Math.min(p[1], q[1]) <= r[1] && r[1] <= Math.max(p[1], q[1]);
|
|
437
|
+
return (d1 === 0 && on(c, d, a)) || (d2 === 0 && on(c, d, b)) || (d3 === 0 && on(a, b, c)) || (d4 === 0 && on(a, b, d));
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** Why a polygon of distinct vertices is not simple — two edges that are not neighbours meet, or a neighbour folds back over its edge — or null. */
|
|
441
|
+
function selfCrossingWhy(pts: readonly Pt[]): string | null {
|
|
442
|
+
const n = pts.length;
|
|
443
|
+
for (let i = 0; i < n; i++) {
|
|
444
|
+
const a = pts[i];
|
|
445
|
+
const b = pts[(i + 1) % n];
|
|
446
|
+
// A neighbour folding back: the next edge runs back along this one.
|
|
447
|
+
const c = pts[(i + 2) % n];
|
|
448
|
+
if (cross3(a, b, c) === 0 && (c[0] - b[0]) * (a[0] - b[0]) + (c[1] - b[1]) * (a[1] - b[1]) > 0) return `edges ${i} and ${(i + 1) % n} fold back over each other at vertex ${(i + 1) % n}`;
|
|
449
|
+
for (let j = i + 2; j < n; j++) {
|
|
450
|
+
if (i === 0 && j === n - 1) continue;
|
|
451
|
+
if (segmentsMeet(a, b, pts[j], pts[(j + 1) % n])) return `edges ${i} and ${j} cross or touch (the polygon is not simple)`;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** The polygon with every vertex dropped that lies on the line through its two neighbours — the area is the same. */
|
|
458
|
+
function withoutCollinear(pts: readonly Pt[]): Pt[] {
|
|
459
|
+
let out = [...pts];
|
|
460
|
+
for (let changed = true; changed && out.length > 3; ) {
|
|
461
|
+
changed = false;
|
|
462
|
+
for (let i = 0; i < out.length; i++) {
|
|
463
|
+
const prev = out[(i + out.length - 1) % out.length];
|
|
464
|
+
const next = out[(i + 1) % out.length];
|
|
465
|
+
if (cross3(prev, out[i], next) === 0) {
|
|
466
|
+
out = [...out.slice(0, i), ...out.slice(i + 1)];
|
|
467
|
+
changed = true;
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** The convex hull (Andrew's monotone chain), counter-clockwise, no collinear vertex, from the lowest-leftmost point. */
|
|
476
|
+
function convexHull(pts: readonly Pt[]): Pt[] {
|
|
477
|
+
const sorted = [...pts].sort((p, q) => p[0] - q[0] || p[1] - q[1]);
|
|
478
|
+
const half = (list: readonly Pt[]): Pt[] => {
|
|
479
|
+
const h: Pt[] = [];
|
|
480
|
+
for (const p of list) {
|
|
481
|
+
while (h.length >= 2 && cross3(h[h.length - 2], h[h.length - 1], p) <= 0) h.pop();
|
|
482
|
+
h.push(p);
|
|
483
|
+
}
|
|
484
|
+
h.pop();
|
|
485
|
+
return h;
|
|
486
|
+
};
|
|
487
|
+
return [...half(sorted), ...half([...sorted].reverse())];
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function ccw(pts: readonly Pt[]): Pt[] {
|
|
491
|
+
const flat = pts.flat();
|
|
492
|
+
return signedArea2(flat) < 0 ? [...pts].reverse() : [...pts];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* A simple counter-clockwise polygon with no collinear vertex cut into convex
|
|
497
|
+
* pieces: ear clipping into triangles (the first ear from vertex 0, walking
|
|
498
|
+
* forward), then every diagonal removed whose two pieces merge into a strictly
|
|
499
|
+
* convex polygon (Hertel–Mehlhorn), in the order the diagonals were cut. Each
|
|
500
|
+
* piece is counter-clockwise, over the polygon's own vertices — none added,
|
|
501
|
+
* every one used.
|
|
502
|
+
*/
|
|
503
|
+
export function convexPieces(polygon: readonly number[]): number[][] {
|
|
504
|
+
return piecesOf(ccw(withoutCollinear(pointsOf(polygon)))).map((piece) => piece.flat());
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function piecesOf(pts: readonly Pt[]): Pt[][] {
|
|
508
|
+
const n = pts.length;
|
|
509
|
+
const idx = pts.map((_, i) => i);
|
|
510
|
+
const tris: number[][] = [];
|
|
511
|
+
const inTri = (p: Pt, a: Pt, b: Pt, c: Pt): boolean => cross3(a, b, p) >= 0 && cross3(b, c, p) >= 0 && cross3(c, a, p) >= 0;
|
|
512
|
+
while (idx.length > 3) {
|
|
513
|
+
let cut = false;
|
|
514
|
+
for (let k = 0; k < idx.length; k++) {
|
|
515
|
+
const i0 = idx[(k + idx.length - 1) % idx.length];
|
|
516
|
+
const i1 = idx[k];
|
|
517
|
+
const i2 = idx[(k + 1) % idx.length];
|
|
518
|
+
if (cross3(pts[i0], pts[i1], pts[i2]) <= 0) continue;
|
|
519
|
+
if (idx.some((j) => j !== i0 && j !== i1 && j !== i2 && inTri(pts[j], pts[i0], pts[i1], pts[i2]))) continue;
|
|
520
|
+
tris.push([i0, i1, i2]);
|
|
521
|
+
idx.splice(k, 1);
|
|
522
|
+
cut = true;
|
|
523
|
+
break;
|
|
524
|
+
}
|
|
525
|
+
if (!cut) throw new Error(`convexPieces: no ear left among vertices [${idx.join(', ')}] of a polygon the caller held simple — a defect in src/core/clipping.ts`);
|
|
526
|
+
}
|
|
527
|
+
tris.push([...idx]);
|
|
528
|
+
// Hertel–Mehlhorn: merge across a shared edge while the union stays strictly convex.
|
|
529
|
+
let pieces = tris.map((t) => [...t]);
|
|
530
|
+
const strictlyConvex = (ring: readonly number[]): boolean => ring.every((v, i) => cross3(pts[ring[(i + ring.length - 1) % ring.length]], pts[v], pts[ring[(i + 1) % ring.length]]) > 0);
|
|
531
|
+
for (let merged = true; merged; ) {
|
|
532
|
+
merged = false;
|
|
533
|
+
search: for (let a = 0; a < pieces.length; a++) {
|
|
534
|
+
for (let b = a + 1; b < pieces.length; b++) {
|
|
535
|
+
const A = pieces[a];
|
|
536
|
+
const B = pieces[b];
|
|
537
|
+
for (let i = 0; i < A.length; i++) {
|
|
538
|
+
const u = A[i];
|
|
539
|
+
const v = A[(i + 1) % A.length];
|
|
540
|
+
const j = B.indexOf(v);
|
|
541
|
+
if (j < 0 || B[(j + 1) % B.length] !== u) continue;
|
|
542
|
+
// A walks u→v, B walks v→u: splice B's other vertices in between.
|
|
543
|
+
const rest: number[] = [];
|
|
544
|
+
for (let k = (j + 2) % B.length; k !== j; k = (k + 1) % B.length) rest.push(B[k]);
|
|
545
|
+
const ring = [...A.slice(0, i + 1), ...rest, ...A.slice(i + 1)];
|
|
546
|
+
if (!strictlyConvex(ring)) continue;
|
|
547
|
+
pieces = [...pieces.slice(0, a), ring, ...pieces.slice(a + 1, b), ...pieces.slice(b + 1)];
|
|
548
|
+
merged = true;
|
|
549
|
+
break search;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (n !== new Set(pieces.flat()).size) throw new Error('convexPieces: a vertex is in no piece — a defect in src/core/clipping.ts');
|
|
555
|
+
return pieces.map((ring) => ring.map((v) => pts[v]));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Which reading a clip is drawn through (this section's `ClipPlan`), from its
|
|
560
|
+
* world polygon and flags. A strictly convex polygon (`convexWhy` null) is the
|
|
561
|
+
* measured rule whatever its flags say but `inverse`; `convex` on any other
|
|
562
|
+
* simple polygon, and `inverse` always, read its convex hull, as the
|
|
563
|
+
* runtime's own documentation of the two flags states.
|
|
564
|
+
*/
|
|
565
|
+
export function clipShapeOf(shape: ClipShape): ClipPlan {
|
|
566
|
+
const pts = pointsOf(shape.polygon);
|
|
567
|
+
if (pts.length < 3) return { kind: 'refused', why: `it has ${pts.length} vertex(es)` };
|
|
568
|
+
const repeated: string[] = [];
|
|
569
|
+
for (let i = 0; i < pts.length; i++) for (let j = i + 1; j < pts.length; j++) if (pts[i][0] === pts[j][0] && pts[i][1] === pts[j][1]) repeated.push(`${i} and ${j}`);
|
|
570
|
+
if (repeated.length > 0) {
|
|
571
|
+
// Consecutive repeats of an otherwise strictly convex polygon: the measured rule, whose zero-length edge has no inside.
|
|
572
|
+
const distinct = pts.filter((p, i) => { const q = pts[(i + 1) % pts.length]; return p[0] !== q[0] || p[1] !== q[1]; });
|
|
573
|
+
if (!shape.inverse && distinct.length === pts.length - repeated.length && distinct.length >= 3 && convexWhy(distinct.flat()) === null) return { kind: 'convex', polygon: shape.polygon };
|
|
574
|
+
return { kind: 'refused', why: `vertices ${repeated.join(', ')} are the same point` };
|
|
575
|
+
}
|
|
576
|
+
const crossing = selfCrossingWhy(pts);
|
|
577
|
+
if (crossing !== null) return { kind: 'refused', why: crossing };
|
|
578
|
+
const simple = withoutCollinear(pts);
|
|
579
|
+
if (cross3(simple[0], simple[1], simple[2]) === 0 && simple.length === 3) return { kind: 'refused', why: 'every vertex lies on one line' };
|
|
580
|
+
if (shape.inverse) return { kind: 'inverse', hull: convexHull(simple) };
|
|
581
|
+
if (convexWhy(shape.polygon) === null) return { kind: 'convex', polygon: shape.polygon };
|
|
582
|
+
if (shape.convex) return { kind: 'convex', polygon: convexHull(simple).flat() };
|
|
583
|
+
const pieces = piecesOf(ccw(simple));
|
|
584
|
+
return pieces.length === 1 ? { kind: 'convex', polygon: pieces[0].flat() } : { kind: 'pieces', pieces };
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/** Clip `input` to the inside of the edge `(p, q)` — one Sutherland–Hodgman step of `clipTriangle`, the same inside test and intersection. */
|
|
588
|
+
function clipHalf(input: readonly Pt[], p: Pt, q: Pt): { points: Pt[]; cut: boolean } {
|
|
589
|
+
const inside = (v: Pt): boolean => (q[0] - p[0]) * (v[1] - p[1]) - (q[1] - p[1]) * (v[0] - p[0]) < 0;
|
|
590
|
+
const out: Pt[] = [];
|
|
591
|
+
let cut = false;
|
|
592
|
+
for (let k = 0; k < input.length; k++) {
|
|
593
|
+
const s = input[k];
|
|
594
|
+
const e = input[(k + 1) % input.length];
|
|
595
|
+
const si = inside(s);
|
|
596
|
+
const ei = inside(e);
|
|
597
|
+
if (si) {
|
|
598
|
+
if (ei) out.push(e);
|
|
599
|
+
else {
|
|
600
|
+
out.push(intersect(s, e, p, q));
|
|
601
|
+
cut = true;
|
|
602
|
+
}
|
|
603
|
+
} else {
|
|
604
|
+
cut = true;
|
|
605
|
+
if (ei) out.push(intersect(s, e, p, q), e);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return { points: out, cut };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* An attachment's triangles clipped against any clip the core draws
|
|
613
|
+
* (`clipShapeOf`): the measured convex rule for a strictly convex polygon —
|
|
614
|
+
* `clipTriangles` itself, so the oracle's rows stand — and otherwise, triangle
|
|
615
|
+
* by triangle in the attachment's order, each convex region of the plan in
|
|
616
|
+
* turn, every region cut exactly as a convex clip cuts it (a region a triangle
|
|
617
|
+
* lies wholly inside keeps its corners and their UVs; a cut one is weighed and
|
|
618
|
+
* fanned from its first point). `null` where the plan is refused.
|
|
619
|
+
*
|
|
620
|
+
* `inverse`: the regions are the triangle's parts outside the clockwise hull,
|
|
621
|
+
* one per hull edge in the walk's order — outside edge `i` and inside every
|
|
622
|
+
* edge before it — so they tile the triangle minus the hull without overlap.
|
|
623
|
+
*/
|
|
624
|
+
export function clipThrough(
|
|
625
|
+
shape: ClipShape,
|
|
626
|
+
vertices: readonly number[],
|
|
627
|
+
triangles: readonly number[],
|
|
628
|
+
uvs: readonly number[],
|
|
629
|
+
clip: TriangleClipper = clipTriangles,
|
|
630
|
+
reading: { dropRegion?: number } = {},
|
|
631
|
+
): ClipResult | null {
|
|
632
|
+
const plan = clipShapeOf(shape);
|
|
633
|
+
if (plan.kind === 'refused') return null;
|
|
634
|
+
if (plan.kind === 'convex') return clip(plan.polygon, vertices, triangles, uvs);
|
|
635
|
+
const f = Math.fround;
|
|
636
|
+
const outV: number[] = [];
|
|
637
|
+
const outUv: number[] = [];
|
|
638
|
+
const outT: number[] = [];
|
|
639
|
+
const sources: number[] = [];
|
|
640
|
+
let clipped = false;
|
|
641
|
+
const regions = plan.kind === 'pieces' ? plan.pieces.map((piece) => clockwise(piece.flat(), {})) : null;
|
|
642
|
+
const hull = plan.kind === 'inverse' ? clockwise(plan.hull.flat(), {}) : [];
|
|
643
|
+
for (let k = 0; k + 2 < triangles.length; k += 3) {
|
|
644
|
+
const idx = [triangles[k], triangles[k + 1], triangles[k + 2]];
|
|
645
|
+
const tri = idx.map((i): Pt => [vertices[2 * i], vertices[2 * i + 1]]);
|
|
646
|
+
const tuv = idx.map((i): Pt => [f(uvs[2 * i]), f(uvs[2 * i + 1])]);
|
|
647
|
+
const parts: Array<{ points: Pt[]; cut: boolean }> = [];
|
|
648
|
+
if (regions !== null) for (const poly of regions) parts.push(clipTriangle(tri, poly, {}));
|
|
649
|
+
else {
|
|
650
|
+
let remaining: Pt[] = [...tri];
|
|
651
|
+
let cutSoFar = false;
|
|
652
|
+
for (let i = 0; i < hull.length && remaining.length > 0; i++) {
|
|
653
|
+
const p = hull[i];
|
|
654
|
+
const q = hull[(i + 1) % hull.length];
|
|
655
|
+
const outside = clipHalf(remaining, q, p);
|
|
656
|
+
parts.push({ points: outside.cut || cutSoFar ? outside.points : [...tri], cut: outside.cut || cutSoFar });
|
|
657
|
+
const inside = clipHalf(remaining, p, q);
|
|
658
|
+
cutSoFar ||= inside.cut;
|
|
659
|
+
remaining = inside.points;
|
|
660
|
+
}
|
|
661
|
+
clipped = true;
|
|
662
|
+
}
|
|
663
|
+
const [a, b, c] = tri;
|
|
664
|
+
const d0 = b[1] - c[1];
|
|
665
|
+
const d1 = c[0] - b[0];
|
|
666
|
+
const d2 = a[0] - c[0];
|
|
667
|
+
const d4 = c[1] - a[1];
|
|
668
|
+
const d = 1 / (d0 * d2 + d1 * (a[1] - c[1]));
|
|
669
|
+
for (const [r, { points, cut }] of parts.entries()) {
|
|
670
|
+
clipped ||= cut;
|
|
671
|
+
// `CL05`'s plant: one region of the plan dropped. Nothing but a plant sets it.
|
|
672
|
+
if (r === reading.dropRegion) continue;
|
|
673
|
+
if (points.length < 3) continue;
|
|
674
|
+
const base = outV.length / 2;
|
|
675
|
+
if (!cut) {
|
|
676
|
+
tri.forEach(([x, y], k2) => {
|
|
677
|
+
outV.push(f(x), f(y));
|
|
678
|
+
outUv.push(tuv[k2][0], tuv[k2][1]);
|
|
679
|
+
});
|
|
680
|
+
} else {
|
|
681
|
+
for (const [x, y] of points) {
|
|
682
|
+
const wa = (d0 * (x - c[0]) + d1 * (y - c[1])) * d;
|
|
683
|
+
const wb = (d4 * (x - c[0]) + d2 * (y - c[1])) * d;
|
|
684
|
+
const wc = 1 - wa - wb;
|
|
685
|
+
outV.push(f(x), f(y));
|
|
686
|
+
outUv.push(f(tuv[0][0] * wa + tuv[1][0] * wb + tuv[2][0] * wc), f(tuv[0][1] * wa + tuv[1][1] * wb + tuv[2][1] * wc));
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
for (let j = 1; j + 1 < points.length; j++) {
|
|
690
|
+
outT.push(base, base + j, base + j + 1);
|
|
691
|
+
sources.push(k / 3);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return { clipped, vertices: outV, uvs: outUv, triangles: outT, sources };
|
|
696
|
+
}
|