rig-parts 1.65.0 → 1.67.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -272,7 +272,7 @@ Use the graph's node classes and the host's `/object_info` when checking the ada
272
272
  ```sh
273
273
  rig-parts inputs --source painting.png --config config.json --out inputs # st_input_full.png; the config needs assemble.rig_scale
274
274
  # See-through on st_input_full.png (external, or `rig-parts comfy seethrough`) -> layers/full
275
- rig-parts layers layers/full # every layer: box, opaque px, depth, plausibility figures; WARN lines
275
+ rig-parts layers layers/full # drawn(a>8)/opaque(a>=250) boxes, CORE body-group height in input-canvas px, WARNs
276
276
  rig-parts propose --head-box --full layers/full --canvas 1664x2432
277
277
  # -> seethrough.head_box into config.json
278
278
  rig-parts inputs --source painting.png --config config.json --out inputs # now st_input_head.png too
@@ -287,11 +287,12 @@ rig-parts assemble --source painting.png --full layers/full --head layers/head -
287
287
  # -> a large red hole neither run holds? add an assemble.patches entry (cut from the painting) and assemble again
288
288
  rig-parts propose --parts work/rig --source painting.png --out work
289
289
  # -> proposal.json, basis.json (what each bone rests on) and render/landmarks.png; per-bone coverage lines;
290
- # correct it, copy bones/meshes/regions/limbs/motion/constraints/turn/proposed_rig_scale into config.json, each it wrote
290
+ # correct it, copy bones/meshes/regions/limbs/motion/constraints/turn/motion_seams/proposed_rig_scale into config.json, each it wrote
291
291
  # (limbs only with --keypoints, constraints only with --cloth, turn only with --full/--head;
292
292
  # a limbs block left out is refused next)
293
293
  rig-parts propose --parts work/rig --source painting.png --out work --from-config config.json
294
- # -> LINT lines; exit 1 while any remain
294
+ # -> LINT lines; exit 1 while any remain (takes --parts, --source, --out, --from-config and optional --keypoints [--person] only;
295
+ # proposes nothing, so --full, --head, --cloth, --write-config, --wrist-snap, --grid-rule, --only-missing and --compare are refused)
295
296
  rig-parts build --config config.json --source painting.png --full layers/full --head layers/head --out out --loop
296
297
  # -> read out/check/check.json; every FAIL line names what has to change
297
298
  ```
@@ -333,7 +334,7 @@ agent skill.
333
334
  | `inputs --source <png> --config <json> --out <dir>` | the two images See-through is fed: the painting on a white square, and the head box's crop once the config has one |
334
335
  | `comfy paint --config --out [--host]` | optional: generate the painting on a ComfyUI box from the config's `generation` block |
335
336
  | `comfy seethrough --image --out [--host]` | optional: run the ComfyUI See-through wrapper on one image and write the form `layers` reads |
336
- | `layers <dir \| layers.json \| file.psd>` | print every layer of a decomposition: draw order, name, tag group, box, size, opaque pixels, depth, and its translucent, background and area figures; a `WARN` line for a layer `--propose-plan` will leave out |
337
+ | `layers <dir \| layers.json \| file.psd>` | keep the existing columns (`opaque_px` still means alpha >8) and append source-alpha boxes `drawn(a>8)` / `opaque(a>=250)` with exclusive right/bottom; name absent boxes, report the existing body-group union height at **CORE_ALPHA (>=250)** in **input-canvas px**, and show its >8 union separately; no anatomical height, ownership/erosion or painting registration is inferred ([units and rig-scale guidance](docs/AUTHORING.md#4-the-command-order)) |
337
338
  | `sheet --source <png> --layers <path>… --out <png>` | a labelled contact sheet of the painting and every layer or part |
338
339
  | `assemble --propose-plan … [--write-config <config>]` | propose `assemble.plan` and `extend_below_crop` from the two runs, leaving out an implausible layer with a note naming the rule, naming in a note any full-run body no planned part covers, and naming every layer no row takes with why (empty, taken from the other run only, a plausibility rule; `nose`, never planned, with the row that lets `propose` read the head's pitch) |
339
340
  | `assemble --source --full --head --config --out [--seam] [--project]` | merge the two runs into rig-space parts, `parts.json`, the recomposite and its error map, and list the uncovered holes |
@@ -341,7 +342,7 @@ agent skill.
341
342
  | `propose --parts --source --out [--compare <config>] [--write-config <config> [--only-missing]]` | propose bones, meshes, regions and an idle; draw the overlay; print, per bone, which check read it or that none did and why (its role, its kind of segment), and write `basis.json` beside the proposal — what each bone rests on (a joint, a part's mask, a ratio, other bones) and, with `--compare`, whether the config's bone differs |
342
343
  | `propose … --keypoints <file> [--person <id>]` | read a posed figure's joints from one explicit file (space, image size, people, each joint observed, occluded or missing, its source): a joint with a position places the neck, hip, chest or a sleeve chain as given, a note says how every joint was used or which rule stood in, and LINT reads the torso and sleeves along the joints instead of by screen y ([AUTHORING §3](docs/AUTHORING.md)) |
343
344
  | `propose … --grid-rule figure\|part` | how each drafted lattice's spacing is chosen: `figure` (the default) one spacing for the figure, round(H / 37) rig px, a part already coarser keeping its own, with a note on its calibration and cost; `part` the reference's rule, a tenth of the part's short side (the oracle examples are proposed under it) |
344
- | `propose … --from-config <config>` | draw and LINT the config's current bones (by the joints too, given `--keypoints`) |
345
+ | `propose --parts --source --out --from-config <config> [--keypoints <file> [--person <id>]]` | draw and LINT the config's current bones (by the joints too, given `--keypoints`); proposes nothing, so takes only these flags (not `--full`, `--head`, `--cloth`, `--write-config`, `--wrist-snap`, `--grid-rule`, `--only-missing` or `--compare`) |
345
346
  | `compare --left <file> --right <file> [--map <bonemap.json>]` | compare two skeletons — a config, a proposal or a `rig.json` — bone by bone: origin, parent, tip, length and direction per pair, the bones neither side pairs, the required ones missing, and each side's roles read off the spec; no frame is guessed, and nothing pairs by resemblance (`propose --compare` reads origins only) |
346
347
  | `rig --config --parts --out [--idle-keys ctl\|direct]` | author `rig.json` + `motion.json`, written only after rig-c's gate is green; each mesh is in the mode its config entry names — `grid` (the lattice, what `propose` writes), `contour` (the traced outline with declared interior points) or `auto` (the contour mesh at alpha 1 and above, reduced and locally refined by rig-c's `reduceMesh` under the author's declared bounds, written only when its motion against the unreduced source on the idle passes the author's `motion` bounds through rig-c's `compareMeshesInMotion`; [AUTHORING §3](docs/AUTHORING.md)); every chain link is turned along its chain and carries its `length`, so a physics constraint added later has a lever, with every offset under it in the turned frame so nothing moves; `--idle-keys` says whether the idle's keys on mesh-driving bones go through `<bone>_ctl` parents (default) or stay on the bones with `invariants.idleDrivesMeshes` declared; the config's `constraints` go to rigc as written, and a two-bone ik over keyed links needs `--idle-keys direct` (on `rig` or `build`) |
347
348
  | `check --rig --out [--parts] [--source] [--requirements]` | build packed, gated under `rig-play`, render the idle, measure seam, loop and the six judgement lines (mesh texture stretch among them, from the idle's `render --geometry`), and report the recomposite's holes from `parts.json`; on a rig spec with no `parts.json` or no `idle` (a merged rig, issue #77) the gate still runs and every line that needs the missing input says SKIP by name; `--source <painting.png>` adds `SETUP_POSE_VS_SOURCE`, the setup pose against the painting by assemble's recomposite figures; `--requirements <scene.json>` measures what a scene declares of the motion — a contact, a follow fraction, an aim, a joint range, a mesh's stretch, the seam between two parts over the motion — each against the author's own bar, from rig-c's posed frames, one line each, PASS, FAIL or NOT MEASURABLE (AUTHORING §7, *Declared requirements*) |
package/cli.ts CHANGED
@@ -28,7 +28,7 @@ import { GUIDE_DOC, type GuideAnswer, guideRule, guideSection } from './src/guid
28
28
  import { proposeHeadBox } from './src/headbox.ts';
29
29
  import { makeInputs } from './src/inputs.ts';
30
30
  import { idleKeysOf, isIdleKeys } from './src/rig.ts';
31
- import { depthSkipLine, exportLayers, figuresPhrase, hiddenLayerLine, implausibleRules, layerFigures, type LayerSet, layerSetDiff, pct, readLayers, readWrapperLayers, ruleSummary, templateLayerNames, templatePsdBytes, times } from './src/layers.ts';
31
+ import { bodyCoreBox, bodyOpaqueBox, CORE_ALPHA, depthSkipLine, exportLayers, figuresPhrase, hiddenLayerLine, implausibleRules, layerCoreBox, layerFigures, layerOpaqueBox, type LayerSet, layerSetDiff, type OpaqueBox, OPAQUE_ALPHA_ABOVE, pct, readLayers, readWrapperLayers, ruleSummary, templateLayerNames, templatePsdBytes, times } from './src/layers.ts';
32
32
  import { checkImageSize, choosePerson, KEYPOINTS_SPEC, loadKeypoints, type RigJoints, toRigJoints } from './src/keypoints.ts';
33
33
  import { basisLines, branchSqueezeLine, checkProposal, compare, compareLines, DEFAULT_GRID_RULE, draftTurn, drawLandmarks, FIGURE_GRID_DIVISOR, FRINGE_STEP_PX, GRID_RULES, type GridRule, HIP_MIN_FRACTION, type DraftLintFinding, lint, lintLine, type PartSet, PROPOSAL_CONFIG_SECTIONS, proposalAsConfig, proposeWithBasis, readPartSet, serializeProposal, withTurn, WRIST_ON_PART_PX } from './src/propose.ts';
34
34
  import { BASIS_FILE, BASIS_SPEC, basisFile, basisLine, coverageLines, legChainChoice, serializeBasis } from './src/diagnostics.ts';
@@ -79,6 +79,11 @@ usage:
79
79
  where something covers it: listed apart, never drawn, never a part. An
80
80
  input with no depth (a PSD, or depth_median null on every layer, whose
81
81
  manifest order is then the draw order) prints one SKIP depth line.
82
+ Trailing boxes name both source-alpha cuts: drawn(a>8), and opaque
83
+ (a>=250, CORE_ALPHA). The legacy opaque_px still counts alpha above 8.
84
+ Right/bottom are exclusive, in INPUT-CANVAS px; absent boxes are named.
85
+ The body-group union height uses alpha >=250, with the >8 union shown
86
+ separately: no cleanup, ownership, anatomical height or painting map.
82
87
  Refuses, by name, a missing file, an unknown tag, a PNG whose
83
88
  size is not its box, and anything else outside the input contract —
84
89
  never a WARN.
@@ -268,9 +273,10 @@ usage:
268
273
 
269
274
  rig-parts propose --parts <dir> --source <painting.png> --out <dir> --from-config <config.json>
270
275
  Draw the config's CURRENT bones instead (<out>/render/landmarks_config.png
271
- and _head) and LINT them (by the joints too, given --keypoints), with the
272
- coverage lines after them; no ${BASIS_FILE}, as nothing is proposed.
273
- Exits 1 when any LINT line is printed.
276
+ and _head) and LINT them (by the joints too, given --keypoints [--person]), with the
277
+ coverage lines after them; no ${BASIS_FILE}, as nothing is proposed (takes
278
+ --parts, --source, --out, --from-config and optional --keypoints [--person]
279
+ only). Exits 1 when any LINT line is printed.
274
280
 
275
281
  rig-parts propose … --write-config <config.json>
276
282
  Without --cloth, update an existing config with the whole proposal's
@@ -416,7 +422,8 @@ usage:
416
422
  rig-parts mesh-stats --config <config.json> --parts <dir>
417
423
  Print the vertices and triangles of every mesh the config states, as the
418
424
  rig stage would make them from the parts in --parts (the directory
419
- holding parts.json and parts/<name>.png), and their total, in seconds:
425
+ holding parts.json and parts/ — work/rig from assemble --out work, or
426
+ out from build --out out), and their total, in seconds:
420
427
  the rig stage's own mesh construction, without rigc, without assemble
421
428
  and without check (issue #566). One line per mesh in parts.json's order,
422
429
  "mesh-stats: <part> — <mode> <V> vertices, <T> triangles" (mode lattice,
@@ -993,10 +1000,29 @@ function printLayerTable(set: LayerSet): void {
993
1000
  const head = ['order', 'name', 'group', 'left,top', 'right,bottom', 'size', 'opaque_px', 'depth', 'translucent', 'background', 'area'];
994
1001
  const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
995
1002
  const line = (cells: string[]): string => ` ${cells.map((c, i) => c.padEnd(widths[i])).join(' ')}`.trimEnd();
996
- console.log(line(head));
997
- for (const r of rows) console.log(line(r));
1003
+ // Keep every legacy line byte-for-byte as a prefix; size the trailing fields separately.
1004
+ const legacy = [line(head), ...rows.map(line)];
1005
+ const legacyWidth = Math.max(...legacy.map(l => l.length));
1006
+ const drawn = `drawn(a>${OPAQUE_ALPHA_ABOVE})`;
1007
+ const opaque = `opaque(a>=${CORE_ALPHA})`;
1008
+ const boxes = set.layers.map(l => [layerOpaqueBox(l), layerCoreBox(l)] as const);
1009
+ const opaqueHead = [drawn, opaque].flatMap(label => [`${label}_left,top`, `${label}_right,bottom`, `${label}_size`]);
1010
+ const opaqueRows = boxes.map(pair => pair.flatMap(b => b === null ? ['none', 'none', 'none'] : [`${b.left},${b.top}`, `${b.right},${b.bottom}`, `${b.right - b.left}x${b.bottom - b.top}`]));
1011
+ const opaqueWidths = opaqueHead.map((h, i) => Math.max(h.length, ...opaqueRows.map(r => r[i].length)));
1012
+ for (const [i, cells] of [opaqueHead, ...opaqueRows].entries()) {
1013
+ console.log(`${legacy[i].padEnd(legacyWidth)} ${cells.map((c, j) => c.padEnd(opaqueWidths[j])).join(' ')}`.trimEnd());
1014
+ }
998
1015
  const painted = set.layers.filter((l) => l.opaquePx > 0).length;
999
1016
  console.log(` ${set.layers.length} layer(s): ${painted} with opaque pixels, ${set.layers.length - painted} with none`);
1017
+ console.log(` source-alpha boxes: ${drawn}, ${opaque}; legacy opaque_px remains alpha > ${OPAQUE_ALPHA_ABOVE}; INPUT-CANVAS px; right/bottom exclusive`);
1018
+ for (const [i, [drawnBox, coreBox]] of boxes.entries()) {
1019
+ if (drawnBox === null) console.log(` layer "${set.layers[i].name}": no ${drawn} box (no pixels with alpha > ${OPAQUE_ALPHA_ABOVE})`);
1020
+ if (coreBox === null) console.log(` layer "${set.layers[i].name}": no ${opaque} box (no pixels with alpha >= ${CORE_ALPHA})${drawnBox === null ? '' : `; ${drawn} box is present`}`);
1021
+ }
1022
+ const bodyBounds = (body: OpaqueBox | null): string => body === null ? 'none; height unavailable INPUT-CANVAS px' : `${body.left},${body.top} to ${body.right},${body.bottom}; size ${body.right - body.left}x${body.bottom - body.top}; height ${body.bottom - body.top} INPUT-CANVAS px`;
1023
+ console.log(` body-group union [${opaque}]: ${bodyBounds(bodyCoreBox(set))}; source alpha >= ${CORE_ALPHA} only, not an owned/eroded core or anatomical height`);
1024
+ console.log(` body-group union [${drawn}]: ${bodyBounds(bodyOpaqueBox(set))}; source alpha > ${OPAQUE_ALPHA_ABOVE} only; not an anatomical height`);
1025
+ console.log(' body-group population: drawn group=body (includes hair/head/wings/objects; excludes face/headwear, author and hidden art); before cleanup, resampling or painting registration');
1000
1026
  // Issue #256: a layer the author made is said to be one, so it never reads as See-through's. None, no line.
1001
1027
  for (const l of set.layers) if (l.tag.group === 'author') console.log(` ${authorLayerLine(l.name)}`);
1002
1028
  // Issue #359: a hidden-art layer is listed apart, never in the drawn table. None, no line.
@@ -1351,11 +1377,12 @@ function cmdPropose(args: string[]): number {
1351
1377
  }
1352
1378
  // Issue #671: --only-missing adds entries to an existing config for the parts it attaches by neither section; it
1353
1379
  // belongs to the ordinary proposal's --write-config alone.
1380
+ const fromConfigTakes = '(takes --parts, --source, --out, --from-config and optional --keypoints [--person] only)';
1354
1381
  if (onlyMissing) {
1355
1382
  const mode = 'propose --parts … --write-config <config.json> --only-missing';
1356
1383
  if (headBox) return usage(`--only-missing belongs to ${mode}; --head-box writes seethrough.head_box only`);
1357
1384
  if (!flags.has('--parts')) return usage(`--only-missing belongs to ${mode}; give --parts <dir>, the parts the entries are drafted from`);
1358
- if (flags.has('--from-config')) return usage(`--only-missing belongs to ${mode}; --from-config proposes nothing`);
1385
+ if (flags.has('--from-config')) return usage(`--only-missing belongs to ${mode}; --from-config proposes nothing ${fromConfigTakes}`);
1359
1386
  if (flags.has('--cloth')) return usage(`--only-missing belongs to ${mode}; --cloth writes a new config with the recipe's entries spliced in`);
1360
1387
  if (!flags.has('--write-config')) return usage(`--only-missing belongs to ${mode}; give --write-config <config.json>, the existing config the entries are added to`);
1361
1388
  }
@@ -1400,17 +1427,17 @@ function cmdPropose(args: string[]): number {
1400
1427
  if (partsArg === undefined) return usage('propose needs --parts <dir>, the directory holding parts.json and parts/');
1401
1428
  if (source === undefined) return usage('propose needs --source <painting.png>, drawn under the overlay');
1402
1429
  if (out === undefined) return usage('propose needs --out <dir>');
1403
- if (flags.has('--from-config') && flags.has('--compare')) return usage('--from-config and --compare are two modes; give one');
1430
+ if (flags.has('--from-config') && flags.has('--compare')) return usage(`--from-config and --compare are two modes; give one (--from-config ${fromConfigTakes.slice(1, -1)})`);
1404
1431
  if (flags.has('--person') && !flags.has('--keypoints')) return usage('--person names a person in the --keypoints file; give --keypoints <file>');
1405
1432
  // Issue #251: the See-through runs the turn control's depths are read from; without them nothing is drafted.
1406
- if (flags.has('--from-config') && (flags.has('--full') || flags.has('--head'))) return usage('--full and --head draft the turn control while proposing; --from-config proposes nothing');
1433
+ if (flags.has('--from-config') && (flags.has('--full') || flags.has('--head'))) return usage(`--full and --head draft the turn control while proposing; --from-config proposes nothing ${fromConfigTakes}`);
1407
1434
  // Issue #199: a declared tolerance for a wrist on no handwear part. Whole px above the built-in one: the distance is
1408
1435
  // counted in whole px, so a fraction would bind what its floor binds, and a value at or under the built-in binds nothing.
1409
1436
  const snapArg = flags.get('--wrist-snap');
1410
1437
  let wristSnap: number | null = null;
1411
1438
  if (snapArg !== undefined) {
1412
1439
  if (!flags.has('--keypoints')) return usage('--wrist-snap binds a wrist joint to a handwear part; give --keypoints <file>');
1413
- if (flags.has('--from-config')) return usage('--wrist-snap binds a wrist while proposing; --from-config proposes nothing');
1440
+ if (flags.has('--from-config')) return usage(`--wrist-snap binds a wrist while proposing; --from-config proposes nothing ${fromConfigTakes}`);
1414
1441
  if (!/^[0-9]+$/.test(snapArg) || Number(snapArg) <= WRIST_ON_PART_PX) {
1415
1442
  return usage(`--wrist-snap ${snapArg} is not an integer above ${WRIST_ON_PART_PX}, the px within which a wrist already holds a part`);
1416
1443
  }
@@ -1418,15 +1445,15 @@ function cmdPropose(args: string[]): number {
1418
1445
  }
1419
1446
  // Issue #265: the drafted lattice's spacing; the figure's unless the reference's per-part rule is asked for.
1420
1447
  const ruleArg = flags.get('--grid-rule');
1421
- if (ruleArg !== undefined && flags.has('--from-config')) return usage('--grid-rule chooses the drafted spacing; --from-config proposes nothing');
1448
+ if (ruleArg !== undefined && flags.has('--from-config')) return usage(`--grid-rule chooses the drafted spacing; --from-config proposes nothing ${fromConfigTakes}`);
1422
1449
  if (ruleArg !== undefined && !(GRID_RULES as readonly string[]).includes(ruleArg)) return usage(`--grid-rule ${ruleArg}; one of ${GRID_RULES.join(', ')} is required`);
1423
1450
  const gridRule = (ruleArg ?? DEFAULT_GRID_RULE) as GridRule;
1424
1451
  // Issue #364: the cloth and hair recipe, read from a config's cloth block; without it nothing changes.
1425
1452
  const clothArg = flags.get('--cloth');
1426
- if (clothArg !== undefined && flags.has('--from-config')) return usage('--cloth drafts trees while proposing; --from-config proposes nothing');
1453
+ if (clothArg !== undefined && flags.has('--from-config')) return usage(`--cloth drafts trees while proposing; --from-config proposes nothing ${fromConfigTakes}`);
1427
1454
  // Issue #537: the --cloth config's own text with what its recipe drafts spliced in, beside proposal.json.
1428
1455
  const writeArg = flags.get('--write-config');
1429
- if (writeArg !== undefined && flags.has('--from-config')) return usage('--write-config writes what the recipe drafts into its config; --from-config proposes nothing');
1456
+ if (writeArg !== undefined && flags.has('--from-config')) return usage(`--write-config writes what the recipe drafts into its config; --from-config proposes nothing ${fromConfigTakes}`);
1430
1457
  // Issue #571: the config the recipe is changed from; an entry equal to its recipe's draft is the recipe's, not a hand edit.
1431
1458
  const fromArg = flags.get('--from');
1432
1459
  if (fromArg !== undefined && writeArg === undefined) return usage('--from names the config the recipe is changed from, for --write-config; give --write-config <out.json>');
@@ -1434,7 +1461,7 @@ function cmdPropose(args: string[]): number {
1434
1461
  if (writeArg !== undefined && clothArg !== undefined) {
1435
1462
  const ours = ['proposal.json', BASIS_FILE, 'render/landmarks.png', 'render/landmarks_head.png'].map((f) => resolve(out, f));
1436
1463
  if (existsSync(writeArg) || ours.includes(resolve(writeArg))) {
1437
- throw new PartsError([{ code: 'PROPOSE_WRITE_CONFIG_FRESH', object: writeArg, detail: existsSync(writeArg) ? 'exists; the config is written to a new file, never over the --cloth config or an earlier one' : `is a file this run writes under ${out}; name another file` }]);
1464
+ throw new PartsError([{ code: 'PROPOSE_WRITE_CONFIG_FRESH', object: writeArg, detail: existsSync(writeArg) ? 'exists; the config is written to a new file, never over the --cloth config or an earlier one (unlike assemble --propose-plan and propose without --cloth, which update an existing --config in place)' : `is a file this run writes under ${out}; name another file` }]);
1438
1465
  }
1439
1466
  separateConfigWriteTarget({ path: resolve(writeArg) }, ours, 'PROPOSE_WRITE_CONFIG_FRESH');
1440
1467
  }
@@ -1642,7 +1669,7 @@ function cmdAssemble(args: string[]): number {
1642
1669
  const [source, full, config] = ['--source', '--full', '--config'].map((f) => flags.get(f) as string);
1643
1670
  const head = flags.get('--head') ?? null;
1644
1671
  const writePath = flags.get('--write-config');
1645
- if (writePath !== undefined && resolve(writePath) !== resolve(config)) return usage('--write-config must name the same file as --config for assemble --propose-plan');
1672
+ if (writePath !== undefined && resolve(writePath) !== resolve(config)) return usage('--write-config must name the same file as --config for assemble --propose-plan (assemble --propose-plan updates --config in place; only propose --cloth --write-config writes to a new file)');
1646
1673
  let staged: StagedConfigWrite | null = null;
1647
1674
  try {
1648
1675
  if (propose) {