@adatechnology/game-destruction 0.1.0 → 0.1.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/dist/index.d.ts +19 -2
- package/dist/index.global.js +96 -47
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +96 -47
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -117,6 +117,13 @@ interface DestructionWorld {
|
|
|
117
117
|
ids(): string[];
|
|
118
118
|
/** Move an object (the renderer or physics owns transforms; keep the world in sync so world-space hits land). */
|
|
119
119
|
setTransform(id: string, position: Vec3, rotation: Quat): void;
|
|
120
|
+
/**
|
|
121
|
+
* Move a part's collision shape (raycast, hits, blast and joint distance) to an object-local pose; the part's spec is unchanged. Animations are
|
|
122
|
+
* visual, so a host that plays a clip calls this every frame with `rest + clip offset`. `rotation` is Euler XYZ (radians), default none. For a
|
|
123
|
+
* registered impl it forwards to the optional `DestructibleImpl.setPartPose` and otherwise only moves the world's raycast shape. Dents and holes
|
|
124
|
+
* stay in object space.
|
|
125
|
+
*/
|
|
126
|
+
setPartPose(objectId: string, partId: string, position: Vec3, rotation?: Vec3): void;
|
|
120
127
|
/**
|
|
121
128
|
* Split a loose group into its own object (after a `detach`, when the renderer makes the piece a separate body). The parts move with their
|
|
122
129
|
* state; the new object starts at the old object's transform. Returns the new id.
|
|
@@ -139,14 +146,24 @@ interface DestructionWorld {
|
|
|
139
146
|
disassemble(objectId: string, jointId: string, tool: ToolKind, dt: number): number;
|
|
140
147
|
/** Joints of an object that `tool` can undo now (open joints excluded). */
|
|
141
148
|
workableJoints(objectId: string, tool: ToolKind): JointState[];
|
|
142
|
-
/**
|
|
143
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Joint nearest to a world point (for "aim at the screw"). With `tool`, the nearest open-able joint that tool can undo within `maxDistance`
|
|
151
|
+
* wins; only when none qualifies does it fall back to the nearest joint of any kind.
|
|
152
|
+
*/
|
|
153
|
+
nearestJoint(objectId: string, point: Vec3, maxDistance?: number, tool?: ToolKind): JointState | null;
|
|
144
154
|
/** Put out fire on a part or the whole object, and cool it by `coolC` (default 200). */
|
|
145
155
|
extinguish(objectId: string, partId?: string, coolC?: number): void;
|
|
146
156
|
/** Advance fire, heat transfer, cooling, melting, leaks and delayed explosions. */
|
|
147
157
|
update(dt: number): DestructionEvent[];
|
|
158
|
+
/**
|
|
159
|
+
* The object's LIVE state: the same object the world mutates every update (parts, joints, groups). Treat it as read-only and change the world only
|
|
160
|
+
* through the API (`hit`, `disassemble`, `setLiters`...). Do not keep it across frames to compare "before" and "after": use `snapshot`.
|
|
161
|
+
*/
|
|
148
162
|
state(id: string): ObjectState | null;
|
|
163
|
+
/** The part's LIVE state (see `state`): read-only, mutated by the world. */
|
|
149
164
|
part(id: string, partId: string): PartState | null;
|
|
165
|
+
/** A deep copy of the object's state at this moment: safe to keep, compare and mutate. Null for an unknown id. */
|
|
166
|
+
snapshot(id: string): ObjectState | null;
|
|
150
167
|
/** Repair an object to its spec (test benches use it for "reset"). */
|
|
151
168
|
reset(id: string): void;
|
|
152
169
|
on(type: DestructionEventType | '*', fn: (event: DestructionEvent) => void): () => void;
|
package/dist/index.global.js
CHANGED
|
@@ -1136,7 +1136,7 @@ var GameDestruction = (() => {
|
|
|
1136
1136
|
{
|
|
1137
1137
|
id: "green_wood",
|
|
1138
1138
|
density: 650,
|
|
1139
|
-
hpPerKg:
|
|
1139
|
+
hpPerKg: 5,
|
|
1140
1140
|
penetration: 10,
|
|
1141
1141
|
ductility: 0.5,
|
|
1142
1142
|
fracture: "splinter",
|
|
@@ -1145,7 +1145,8 @@ var GameDestruction = (() => {
|
|
|
1145
1145
|
0.2
|
|
1146
1146
|
],
|
|
1147
1147
|
damageScale: {
|
|
1148
|
-
slash: 1.3
|
|
1148
|
+
slash: 1.3,
|
|
1149
|
+
ballistic: 0.1
|
|
1149
1150
|
},
|
|
1150
1151
|
hitDebris: "splinters",
|
|
1151
1152
|
breakDebris: "leaves",
|
|
@@ -1458,6 +1459,15 @@ var GameDestruction = (() => {
|
|
|
1458
1459
|
var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
1459
1460
|
var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
|
|
1460
1461
|
var clone = (v) => structuredClone(v);
|
|
1462
|
+
var updateBound = (o) => {
|
|
1463
|
+
if (!o.parts.length) return;
|
|
1464
|
+
const c = [0, 0, 0];
|
|
1465
|
+
for (const p of o.parts) for (let k = 0; k < 3; k++) c[k] += p.pos[k] / o.parts.length;
|
|
1466
|
+
let r = 0;
|
|
1467
|
+
for (const p of o.parts) r = Math.max(r, vec.distance(c, p.pos) + p.bound);
|
|
1468
|
+
o.bc = c;
|
|
1469
|
+
o.br = r;
|
|
1470
|
+
};
|
|
1461
1471
|
var AXIS = { x: 0, y: 1, z: 2 };
|
|
1462
1472
|
var createDestruction = (options = {}) => {
|
|
1463
1473
|
const materials = /* @__PURE__ */ new Map();
|
|
@@ -1593,6 +1603,7 @@ var GameDestruction = (() => {
|
|
|
1593
1603
|
mat,
|
|
1594
1604
|
st: pst,
|
|
1595
1605
|
q: quatFromEuler(...ps.rotation ?? [0, 0, 0]),
|
|
1606
|
+
pos: [ps.position[0], ps.position[1], ps.position[2]],
|
|
1596
1607
|
mass0,
|
|
1597
1608
|
fuel0,
|
|
1598
1609
|
area: shapeArea(ps.shape),
|
|
@@ -1631,14 +1642,8 @@ var GameDestruction = (() => {
|
|
|
1631
1642
|
o.byPart.get(r.b).push(j);
|
|
1632
1643
|
o.st.joints.push(jst);
|
|
1633
1644
|
}
|
|
1634
|
-
if (o.
|
|
1635
|
-
|
|
1636
|
-
for (const p of o.parts) for (let k = 0; k < 3; k++) c[k] += p.spec.position[k] / o.parts.length;
|
|
1637
|
-
let r = 0;
|
|
1638
|
-
for (const p of o.parts) r = Math.max(r, vec.distance(c, p.spec.position) + p.bound);
|
|
1639
|
-
o.bc = c;
|
|
1640
|
-
o.br = r;
|
|
1641
|
-
}
|
|
1645
|
+
for (const [jid, c] of carry?.joints ?? []) if (c.st.open && !o.jointMap.has(jid)) o.st.joints.push(c.st);
|
|
1646
|
+
updateBound(o);
|
|
1642
1647
|
o.anyAnchor = o.parts.some((p) => p.anchored);
|
|
1643
1648
|
o.unsup = o.parts.map(() => false);
|
|
1644
1649
|
solveStructure(o);
|
|
@@ -1778,7 +1783,7 @@ var GameDestruction = (() => {
|
|
|
1778
1783
|
if (!p) throw new Error(`unknown part "${id}" in object "${o.id}"`);
|
|
1779
1784
|
return p;
|
|
1780
1785
|
};
|
|
1781
|
-
const partXf = (o, p) => ({ c: toWorld(o.st.position, o.st.rotation, p.
|
|
1786
|
+
const partXf = (o, p) => ({ c: toWorld(o.st.position, o.st.rotation, p.pos), q: quat.mul(o.st.rotation, p.q) });
|
|
1782
1787
|
const objLocal = (o, w) => toLocal(o.st.position, o.st.rotation, w);
|
|
1783
1788
|
const objLocalDir = (o, w) => rotateVec(quat.conjugate(o.st.rotation), w);
|
|
1784
1789
|
const jointWorld = (o, j) => toWorld(o.st.position, o.st.rotation, j.pos);
|
|
@@ -1790,14 +1795,14 @@ var GameDestruction = (() => {
|
|
|
1790
1795
|
const { c, q } = partXf(o, p);
|
|
1791
1796
|
return aabbOf(p.spec.shape, c, q);
|
|
1792
1797
|
};
|
|
1793
|
-
const rayPartRaw = (pos, rot, spec, pq, origin, d) => {
|
|
1794
|
-
const c = toWorld(pos, rot,
|
|
1798
|
+
const rayPartRaw = (pos, rot, spec, pq, origin, d, lp = spec.position) => {
|
|
1799
|
+
const c = toWorld(pos, rot, lp);
|
|
1795
1800
|
const q = quat.mul(rot, pq);
|
|
1796
1801
|
const qi = quat.conjugate(q);
|
|
1797
1802
|
const r = rayShape(spec.shape, rotateVec(qi, vec.sub(origin, c)), rotateVec(qi, d));
|
|
1798
1803
|
return r ? { t0: r.t0, t1: r.t1, n: rotateVec(q, r.n0) } : null;
|
|
1799
1804
|
};
|
|
1800
|
-
const rayPart = (o, p, origin, d) => rayPartRaw(o.st.position, o.st.rotation, p.spec, p.q, origin, d);
|
|
1805
|
+
const rayPart = (o, p, origin, d) => rayPartRaw(o.st.position, o.st.rotation, p.spec, p.q, origin, d, p.pos);
|
|
1801
1806
|
const raySphereMiss = (origin, d, c, r, maxDist) => {
|
|
1802
1807
|
const oc = vec.sub(origin, c);
|
|
1803
1808
|
const b = vec.dot(oc, d);
|
|
@@ -1822,7 +1827,8 @@ var GameDestruction = (() => {
|
|
|
1822
1827
|
for (const im of impls.values()) {
|
|
1823
1828
|
for (const ps of im.impl.describe().parts) {
|
|
1824
1829
|
if (ignore?.has(`${im.impl.id}\0${ps.id}`)) continue;
|
|
1825
|
-
const
|
|
1830
|
+
const ov = im.poses.get(ps.id);
|
|
1831
|
+
const r = rayPartRaw(im.position, im.rotation, ps, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]), origin, d, ov?.pos);
|
|
1826
1832
|
if (!r || r.t0 < 0 || r.t0 > maxDist) continue;
|
|
1827
1833
|
if (!best || r.t0 < best.distance) best = { object: im.impl.id, part: ps.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
|
|
1828
1834
|
}
|
|
@@ -1957,11 +1963,29 @@ var GameDestruction = (() => {
|
|
|
1957
1963
|
collapseCheck(o, src);
|
|
1958
1964
|
checkDestroyed(o, src);
|
|
1959
1965
|
};
|
|
1960
|
-
const
|
|
1966
|
+
const jointReach = (o, j, point) => {
|
|
1967
|
+
const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
|
|
1968
|
+
let d2 = 0;
|
|
1969
|
+
for (let k = 0; k < 3; k++) {
|
|
1970
|
+
const lo = Math.min(Math.max(A.min[k], B.min[k]), Math.min(A.max[k], B.max[k]));
|
|
1971
|
+
const hi = Math.max(Math.max(A.min[k], B.min[k]), Math.min(A.max[k], B.max[k]));
|
|
1972
|
+
const d = point[k] < lo ? lo - point[k] : point[k] > hi ? point[k] - hi : 0;
|
|
1973
|
+
d2 += d * d;
|
|
1974
|
+
}
|
|
1975
|
+
return Math.sqrt(d2);
|
|
1976
|
+
};
|
|
1977
|
+
const jointReachLimit = (o, j) => {
|
|
1978
|
+
const pa = o.parts[j.ia], pb = o.parts[j.ib];
|
|
1979
|
+
const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
|
|
1980
|
+
return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
|
|
1981
|
+
};
|
|
1982
|
+
const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
|
|
1961
1983
|
if (amount <= 0) return;
|
|
1962
|
-
|
|
1984
|
+
const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
|
|
1985
|
+
if (split && mine.length) amount /= mine.length;
|
|
1986
|
+
for (const j of mine) {
|
|
1963
1987
|
if (j.st.open) continue;
|
|
1964
|
-
if (near &&
|
|
1988
|
+
if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
|
|
1965
1989
|
j.st.hp -= amount;
|
|
1966
1990
|
if (j.st.hp <= 0) {
|
|
1967
1991
|
j.st.hp = 0;
|
|
@@ -2080,7 +2104,7 @@ var GameDestruction = (() => {
|
|
|
2080
2104
|
if (mat.fracture !== "shatter") addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
|
|
2081
2105
|
} else addHole(o, p, entry, n, caliber / 1e3, false, void 0, src);
|
|
2082
2106
|
debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
|
|
2083
|
-
wearJoints(o, p, E * 0.
|
|
2107
|
+
wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
|
|
2084
2108
|
const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry, d, E * 0.1, 12, src);
|
|
2085
2109
|
return { penetrated, exitEnergy: penetrated ? E - thr : 0, exitPoint, damage: dmg };
|
|
2086
2110
|
};
|
|
@@ -2155,8 +2179,9 @@ var GameDestruction = (() => {
|
|
|
2155
2179
|
}
|
|
2156
2180
|
for (const im of [...impls.values()]) {
|
|
2157
2181
|
const inRange = im.impl.describe().parts.some((ps) => {
|
|
2158
|
-
const
|
|
2159
|
-
const
|
|
2182
|
+
const ov = im.poses.get(ps.id);
|
|
2183
|
+
const c = toWorld(im.position, im.rotation, ov?.pos ?? ps.position);
|
|
2184
|
+
const q = quat.mul(im.rotation, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]));
|
|
2160
2185
|
return distanceToShape(ps.shape, toLocal(c, q, point)) < radius;
|
|
2161
2186
|
});
|
|
2162
2187
|
if (inRange) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
|
|
@@ -2408,7 +2433,7 @@ var GameDestruction = (() => {
|
|
|
2408
2433
|
const spread = (sources, dt) => {
|
|
2409
2434
|
for (const src of sources) {
|
|
2410
2435
|
if (!src.p.st.burning && !src.p.rt.jet) continue;
|
|
2411
|
-
const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.
|
|
2436
|
+
const sc = toWorld(src.o.st.position, src.o.st.rotation, src.p.pos);
|
|
2412
2437
|
const A = partWorldAabb(src.o, src.p);
|
|
2413
2438
|
for (const o of objects.values()) {
|
|
2414
2439
|
if (o === src.o) continue;
|
|
@@ -2454,9 +2479,23 @@ var GameDestruction = (() => {
|
|
|
2454
2479
|
register(impl, opts = {}) {
|
|
2455
2480
|
const id = opts.id ?? impl.id;
|
|
2456
2481
|
if (taken(id)) throw new Error(`object "${id}" already exists`);
|
|
2457
|
-
impls.set(id, { impl, position: opts.position ?? [0, 0, 0], rotation: opts.rotation ?? [0, 0, 0, 1] });
|
|
2482
|
+
impls.set(id, { impl, position: opts.position ?? [0, 0, 0], rotation: opts.rotation ?? [0, 0, 0, 1], poses: /* @__PURE__ */ new Map() });
|
|
2458
2483
|
return id;
|
|
2459
2484
|
},
|
|
2485
|
+
setPartPose(objectId, partId, position, rotation) {
|
|
2486
|
+
const q = quatFromEuler(...rotation ?? [0, 0, 0]);
|
|
2487
|
+
const im = impls.get(objectId);
|
|
2488
|
+
if (im) {
|
|
2489
|
+
im.impl.setPartPose?.(partId, position, rotation);
|
|
2490
|
+
im.poses.set(partId, { pos: [position[0], position[1], position[2]], q });
|
|
2491
|
+
return;
|
|
2492
|
+
}
|
|
2493
|
+
const o = getObj(objectId);
|
|
2494
|
+
const p = getPart(o, partId);
|
|
2495
|
+
p.pos = [position[0], position[1], position[2]];
|
|
2496
|
+
p.q = q;
|
|
2497
|
+
updateBound(o);
|
|
2498
|
+
},
|
|
2460
2499
|
remove(id) {
|
|
2461
2500
|
objects.delete(id);
|
|
2462
2501
|
impls.delete(id);
|
|
@@ -2483,9 +2522,12 @@ var GameDestruction = (() => {
|
|
|
2483
2522
|
const moved = new Set(names);
|
|
2484
2523
|
const nid = newId ?? nextId();
|
|
2485
2524
|
if (objects.has(nid)) throw new Error(`object "${nid}" already exists`);
|
|
2486
|
-
const carryFor = (keep) => ({
|
|
2525
|
+
const carryFor = (keep, keepOpen = () => false) => ({
|
|
2487
2526
|
parts: new Map(o.parts.filter((p) => keep(p.spec.id)).map((p) => [p.spec.id, { st: p.st, rt: p.rt }])),
|
|
2488
|
-
joints: new Map(
|
|
2527
|
+
joints: new Map([
|
|
2528
|
+
...o.joints.filter((j) => keep(j.spec.a) && keep(j.spec.b) || j.st.open && keepOpen(j.spec.a, j.spec.b)).map((j) => [j.spec.id, { st: j.st, step: j.step }]),
|
|
2529
|
+
...o.st.joints.filter((st) => st.open && !o.jointMap.has(st.id) && keepOpen(st.a, st.b)).map((st) => [st.id, { st, step: 0 }])
|
|
2530
|
+
]),
|
|
2489
2531
|
collapsed: [...o.collapsed].filter(keep)
|
|
2490
2532
|
});
|
|
2491
2533
|
const sub = (keep) => {
|
|
@@ -2497,8 +2539,8 @@ var GameDestruction = (() => {
|
|
|
2497
2539
|
const inNew = (pid) => moved.has(pid);
|
|
2498
2540
|
const inOld = (pid) => !moved.has(pid);
|
|
2499
2541
|
const pos = o.st.position, rot = o.st.rotation;
|
|
2500
|
-
const fresh = build(sub(inNew), nid, pos, rot, carryFor(inNew));
|
|
2501
|
-
const rest = build(sub(inOld), id, pos, rot, carryFor(inOld));
|
|
2542
|
+
const fresh = build(sub(inNew), nid, pos, rot, carryFor(inNew, (a, b) => inNew(a) && inNew(b)));
|
|
2543
|
+
const rest = build(sub(inOld), id, pos, rot, carryFor(inOld, (a, b) => !(inNew(a) && inNew(b))));
|
|
2502
2544
|
objects.set(id, rest);
|
|
2503
2545
|
objects.set(nid, fresh);
|
|
2504
2546
|
return nid;
|
|
@@ -2596,34 +2638,37 @@ var GameDestruction = (() => {
|
|
|
2596
2638
|
}
|
|
2597
2639
|
return getObj(objectId).joints.filter((j) => !j.st.open && j.spec.tools.includes(tool)).map((j) => j.st);
|
|
2598
2640
|
},
|
|
2599
|
-
nearestJoint(objectId, point, maxDistance = 0.3) {
|
|
2641
|
+
nearestJoint(objectId, point, maxDistance = 0.3, tool) {
|
|
2600
2642
|
const im = impls.get(objectId);
|
|
2643
|
+
const cands = [];
|
|
2601
2644
|
if (im) {
|
|
2602
|
-
|
|
2603
|
-
let bd2 = maxDistance;
|
|
2645
|
+
const spec = im.impl.describe();
|
|
2604
2646
|
for (const js of im.impl.state().joints) {
|
|
2605
2647
|
if (js.open) continue;
|
|
2606
2648
|
const pos = implJointPos(im, js.a, js.b);
|
|
2607
|
-
const
|
|
2608
|
-
|
|
2609
|
-
bd2 = d;
|
|
2610
|
-
best2 = js;
|
|
2611
|
-
}
|
|
2649
|
+
const j = (spec.joints ?? []).find((x) => (x.id ?? `${x.a}~${x.b}`) === js.id);
|
|
2650
|
+
cands.push({ st: js, d: pos ? vec.distance(pos, point) : Infinity, ok: !!j && (j.tools ?? JOINT_DEFAULTS[j.kind].tools).includes(tool) });
|
|
2612
2651
|
}
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
for (const j of o.joints) {
|
|
2619
|
-
if (j.st.open) continue;
|
|
2620
|
-
const d = vec.distance(jointWorld(o, j), point);
|
|
2621
|
-
if (d <= bd) {
|
|
2622
|
-
bd = d;
|
|
2623
|
-
best = j.st;
|
|
2652
|
+
} else {
|
|
2653
|
+
const o = getObj(objectId);
|
|
2654
|
+
for (const j of o.joints) {
|
|
2655
|
+
if (j.st.open) continue;
|
|
2656
|
+
cands.push({ st: j.st, d: vec.distance(jointWorld(o, j), point), ok: tool ? j.spec.tools.includes(tool) : true });
|
|
2624
2657
|
}
|
|
2625
2658
|
}
|
|
2626
|
-
|
|
2659
|
+
const pick = (only) => {
|
|
2660
|
+
let best = null;
|
|
2661
|
+
let bd = maxDistance;
|
|
2662
|
+
for (const c of cands) {
|
|
2663
|
+
if (only && !c.ok) continue;
|
|
2664
|
+
if (c.d <= bd) {
|
|
2665
|
+
bd = c.d;
|
|
2666
|
+
best = c.st;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
return best;
|
|
2670
|
+
};
|
|
2671
|
+
return (tool ? pick(true) : null) ?? pick(false);
|
|
2627
2672
|
},
|
|
2628
2673
|
extinguish(objectId, partId, coolC = 200) {
|
|
2629
2674
|
const im = impls.get(objectId);
|
|
@@ -2666,6 +2711,10 @@ var GameDestruction = (() => {
|
|
|
2666
2711
|
}).events;
|
|
2667
2712
|
},
|
|
2668
2713
|
state: (id) => objects.get(id)?.st ?? impls.get(id)?.impl.state() ?? null,
|
|
2714
|
+
snapshot: (id) => {
|
|
2715
|
+
const st = objects.get(id)?.st ?? impls.get(id)?.impl.state();
|
|
2716
|
+
return st ? clone(st) : null;
|
|
2717
|
+
},
|
|
2669
2718
|
part: (id, partId) => objects.get(id)?.byId.get(partId)?.st ?? impls.get(id)?.impl.state().parts.find((p) => p.id === partId) ?? null,
|
|
2670
2719
|
reset(id) {
|
|
2671
2720
|
const im = impls.get(id);
|