partforge 0.53.0 → 0.55.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.
@@ -35,6 +35,15 @@ export function bootOcctKernel(opts?: { fonts?: Record<string, FontSource> }): P
35
35
 
36
36
  // --- the job loop -----------------------------------------------------------
37
37
 
38
+ /**
39
+ * A reference shape for the `inspect` job to score the part's silhouettes against.
40
+ * A `profile` is millimetres and so is compared at absolute scale too; an `image`
41
+ * is a photo mask with no scale, compared on shape alone.
42
+ */
43
+ export type MatchTarget =
44
+ | { kind: "profile"; rings: Array<Array<[number, number]>> }
45
+ | { kind: "image"; mask: { data: Uint8Array; width: number; height: number } };
46
+
38
47
  /** A job the worker loop accepts. */
39
48
  export interface WorkerJob {
40
49
  type: "generate" | "export-stl" | "export-step" | "export-3mf" | "inspect";
@@ -50,6 +59,16 @@ export interface WorkerJob {
50
59
  jobId?: number;
51
60
  /** Single-file export name base. */
52
61
  name?: string;
62
+ /**
63
+ * `inspect`: score the part's six canonical silhouettes against these. Absent or
64
+ * empty leaves `match` off the report entirely.
65
+ *
66
+ * A target that cannot be scored — malformed, or with no foreground to score
67
+ * against — is DROPPED rather than reported as a zero, so `report.match` is not
68
+ * index-aligned with this list. Attribute a result by its `kind` and by the
69
+ * relative order of the targets sharing that kind, never by index.
70
+ */
71
+ matchTargets?: MatchTarget[];
53
72
  }
54
73
 
55
74
  /**
@@ -272,7 +291,16 @@ export function measure(
272
291
  part: PartDefinition,
273
292
  view?: string,
274
293
  params?: ResolvedParams,
275
- opts?: { minWall?: boolean; gapThreshold?: number },
294
+ opts?: {
295
+ minWall?: boolean;
296
+ gapThreshold?: number;
297
+ /**
298
+ * A build of this view the caller already has, measured instead of building a
299
+ * second time. It is trusted, not checked against `view`/`params` — hand in a
300
+ * build of the same view you are asking about.
301
+ */
302
+ built?: BuiltSubPart[];
303
+ },
276
304
  ): MeasureReport;
277
305
 
278
306
  // --- verify -----------------------------------------------------------------
@@ -339,6 +367,93 @@ export function verify(
339
367
  },
340
368
  ): VerifyReport;
341
369
 
370
+ // --- silhouette match scoring -----------------------------------------------
371
+
372
+ /**
373
+ * A binary silhouette. `data` is 0 or 255, one byte per pixel, row 0 at the TOP.
374
+ * `minX`/`minY` are the projected-plane coordinates of the image's BOTTOM-LEFT
375
+ * corner. A mask with no `mmPerPx` carries no scale (a photo), which is what makes
376
+ * the scale-aware comparison unavailable for it.
377
+ */
378
+ export interface SilhouetteMask {
379
+ data: Uint8Array;
380
+ width: number;
381
+ height: number;
382
+ mmPerPx?: number;
383
+ minX?: number;
384
+ minY?: number;
385
+ }
386
+
387
+ /** The six canonical orthographic views a part is rasterized into for matching. */
388
+ export const MATCH_VIEWS: string[];
389
+
390
+ /**
391
+ * Project posed meshes onto one of `MATCH_VIEWS` and scanline-fill the silhouette.
392
+ * `null` when there is nothing to draw or the projection has zero extent.
393
+ */
394
+ export function rasterizeMeshMask(
395
+ meshes: Array<Pick<Mesh, "positions" | "indices">>,
396
+ view: string,
397
+ size?: number,
398
+ ): SilhouetteMask | null;
399
+
400
+ /**
401
+ * Fill a set of closed 2-D rings (millimetres) into a mask. All rings share one
402
+ * even-odd group, so a ring inside another is a hole. `null` when nothing fills.
403
+ */
404
+ export function rasterizeRingsMask(
405
+ rings: Array<Array<[number, number]>>,
406
+ size?: number,
407
+ ): SilhouetteMask | null;
408
+
409
+ /**
410
+ * Per-pixel comparison of the two masks: `0` background, `1` overlap, `2` missing
411
+ * (reference only), `3` excess (candidate only).
412
+ */
413
+ export interface MatchDelta {
414
+ width: number;
415
+ height: number;
416
+ data: Uint8Array;
417
+ }
418
+
419
+ /**
420
+ * How close a candidate silhouette is to a reference one. Shape is compared
421
+ * pose-normalized, so `iou` and `boundaryIoU` ignore position and size. `iouScale`
422
+ * appears only for a scale-aware comparison, which also makes `contourDist` a real
423
+ * millimetre distance instead of a percentage of the reference's bbox diagonal.
424
+ */
425
+ export interface MatchScores {
426
+ iou: number;
427
+ boundaryIoU: number;
428
+ contourDist: number;
429
+ contourUnit: "mm" | "%bbox-diag";
430
+ iouScale?: number;
431
+ delta: MatchDelta;
432
+ }
433
+
434
+ /**
435
+ * Score one candidate mask against a reference. `null` when either has no
436
+ * foreground at all — unscoreable is not the same as scoring zero.
437
+ *
438
+ * `scaleAware` is the caller's promise that both masks are in millimetres; it takes
439
+ * effect only when both actually carry a finite `mmPerPx`.
440
+ */
441
+ export function matchMasks(
442
+ candidate: SilhouetteMask | null | undefined,
443
+ reference: SilhouetteMask | null | undefined,
444
+ opts?: { scaleAware?: boolean },
445
+ ): MatchScores | null;
446
+
447
+ /**
448
+ * Score every view's mask against one reference and name the best. Unscoreable
449
+ * views are left out of `views` entirely; `best` is `null` when none scored.
450
+ */
451
+ export function matchViews(
452
+ viewMasks: Record<string, SilhouetteMask | null>,
453
+ reference: SilhouetteMask | null | undefined,
454
+ opts?: { scaleAware?: boolean },
455
+ ): { best: ({ view: string } & MatchScores) | null; views: Record<string, number> };
456
+
342
457
  // --- rendering --------------------------------------------------------------
343
458
 
344
459
  /** The canonical angle names `renderViews` accepts. */