; body-ladder.scm - Sorting Floor · body shape standard v1
; Drop in Preferences ▸ Folders ▸ Scripts, then
; Filters ▸ Script-Fu ▸ Refresh Scripts.
; Run from Filters ▸ Postal Sim ▸ Build Body Ladder…
;
; Takes the SVG exported by the generator and rebuilds every body as
; three named layers - _glow (13px airbrush pass, blurred), _fill (#808080
; skin mask, crisp) and _line (4px core stroke). Same two-pass order as the
; hand process - core stroke first for the exact edge, airbrush over it for
; weight - reproducible and re-runnable.
;
; Geometry: torso H locked at 300px; heads nestled (cy = HEAD.y − 0.15·ry).
; Eyes: eye_L_/eye_R_ solid fills at ±0.32·rx, −0.06·ry (no iris/hl/lids).
;
; The export emits body_<id> silhouettes, optional head_<id> shapes
; (ellipse / circle / tall oval / capsule), optional eye_L_/eye_R_ fills, and anchor_* guide circles.
; Paths whose names start with body_ or head_ get glow/fill/line; eye_ fills are fill-only.
; Script-Fu skips anchor_* so guides never become ink. Prefer the maintained
; copy at art_src/procgen/ladders/body-ladder.scm (GIMP 2.10 + 3).
; "Columns" counts body/head cells, not paths. Default 1c sheet uses Columns=8.
(define (psim-body-ladder svgfile cell cols core airbrush falloff)
(let* ((w (* cell cols))
(img (car (gimp-image-new w cell RGB)))
(res (gimp-image-import-paths-from-file img svgfile TRUE TRUE))
; GIMP 2.10 instead: (gimp-vectors-import-from-file img svgfile TRUE TRUE)
(parr (cadr res))
(n (vector-length parr))
(i 0))
(gimp-image-undo-disable img)
(gimp-context-push)
(gimp-context-set-antialias TRUE)
(gimp-context-set-feather FALSE)
(gimp-context-set-stroke-method STROKE-LINE)
(gimp-context-set-line-cap-style CAP-ROUND)
(gimp-context-set-line-join-style JOIN-ROUND)
(while (< i n)
(let* ((p (vector-ref parr i))
(nm (car (gimp-item-get-name p)))
(body? (and (>= (string-length nm) 5)
(string=? (substring nm 0 5) "body_")))
(head? (and (>= (string-length nm) 5)
(string=? (substring nm 0 5) "head_")))
(eye? (and (>= (string-length nm) 4)
(string=? (substring nm 0 4) "eye_")))
)
; Skip anchor_* / other non-ink paths
(if (or body? head? eye?)
(let ((glow (car (gimp-layer-new img w cell RGBA-IMAGE
(string-append nm "_glow") 100 LAYER-MODE-NORMAL)))
(line (car (gimp-layer-new img w cell RGBA-IMAGE
(string-append nm "_line") 100 LAYER-MODE-NORMAL))))
; PASS 2 - airbrush, 13px, blurred
(gimp-image-insert-layer img glow 0 0)
(gimp-context-set-foreground '(0 0 0))
(gimp-context-set-line-width airbrush)
(gimp-drawable-edit-stroke-item glow p)
(plug-in-gauss RUN-NONINTERACTIVE img glow falloff falloff 0)
; PASS 3 - #808080 fill on closed silhouettes
(let ((fill (car (gimp-layer-new img w cell RGBA-IMAGE
(string-append nm "_fill") 100 LAYER-MODE-NORMAL))))
(gimp-image-insert-layer img fill 0 0)
(gimp-image-select-item img CHANNEL-OP-REPLACE p)
(gimp-selection-shrink img (/ core 2))
(gimp-context-set-foreground '(128 128 128))
(gimp-drawable-edit-fill fill FILL-FOREGROUND)
(gimp-selection-none img))
; PASS 1 - 4px core stroke
(gimp-image-insert-layer img line 0 0)
(gimp-context-set-foreground '(0 0 0))
(gimp-context-set-line-width core)
(gimp-drawable-edit-stroke-item line p))
TRUE))
(set! i (+ i 1)))
(gimp-context-pop)
(gimp-image-undo-enable img)
(gimp-display-new img)
(gimp-displays-flush)))
(script-fu-register "psim-body-ladder"
"Build Body Ladder..."
"Rebuild the standardized body sheet from an exported ladder SVG."
"Sorting Floor" "Sorting Floor" "2026"
""
SF-FILENAME "Ladder SVG" ""
SF-ADJUSTMENT "Cell size (px)" '(512 64 2048 1 64 0 1)
SF-ADJUSTMENT "Columns" '(8 1 64 1 1 0 1)
SF-ADJUSTMENT "Core stroke (px)" '(4 1 32 1 2 0 1)
SF-ADJUSTMENT "Airbrush (px)" '(13 1 64 1 4 0 1)
SF-ADJUSTMENT "Falloff blur" '(6 0 40 1 4 1 1))
(script-fu-menu-register "psim-body-ladder" "<Image>/Filters/Postal Sim")