Skip to content

Instantly share code, notes, and snippets.

@g-l-i-t-c-h-o-r-s-e
Last active November 27, 2025 08:25
Show Gist options
  • Select an option

  • Save g-l-i-t-c-h-o-r-s-e/546f867cc9b502fd4efb157c89f2c8a5 to your computer and use it in GitHub Desktop.

Select an option

Save g-l-i-t-c-h-o-r-s-e/546f867cc9b502fd4efb157c89f2c8a5 to your computer and use it in GitHub Desktop.
Helper Plugin for ISF Renderer Quartz Composer Plugin
#!/usr/bin/env bash
set -euo pipefail
# --- config (override via env) ---
NAME="${NAME:-ISFInputMapper}"
CLASS="${CLASS:-ISFInputMapperPlugIn}"
# All source files for this plug-in (override with SRCS in env if needed)
# By default we build the main plug-in class and its settings view controller.
SRCS=(${SRCS:-${CLASS}.m ISFInputMapperPlugInViewController.m})
PLUG="$NAME.plugin"
OUT="$(pwd)/build-manual"
INST="$HOME/Library/Graphics/Quartz Composer Plug-Ins"
XCODE_APP="${XCODE_APP:-/Applications/Xcode_9.4.1.app}"
DEV="$XCODE_APP/Contents/Developer"
SDKDIR="$DEV/Platforms/MacOSX.platform/Developer/SDKs"
# Allow env to override; if still empty later, we auto-select based on SDK
DO_I386="${DO_I386:-}"
DO_X64="${DO_X64:-1}"
# Clean output directory first (set CLEAN=0 to keep)
CLEAN="${CLEAN:-1}"
# Prefer 10.14, fall back to 10.13, else xcrun
SDK="${SDK:-}"
if [[ -z "${SDK}" ]]; then
if [[ -d "$SDKDIR/MacOSX10.14.sdk" ]]; then SDK="$SDKDIR/MacOSX10.14.sdk"
elif [[ -d "$SDKDIR/MacOSX10.13.sdk" ]]; then SDK="$SDKDIR/MacOSX10.13.sdk"
else SDK="$(xcrun --sdk macosx --show-sdk-path 2>/dev/null || true)"
fi
fi
[[ -d "$DEV" ]] || { echo "Xcode not found: $XCODE_APP"; exit 1; }
# Verify all sources exist
for s in "${SRCS[@]}"; do
[[ -f "$s" ]] || { echo "Source not found: $s"; exit 1; }
done
[[ -n "${SDK:-}" && -d "$SDK" ]] || { echo "macOS SDK not found."; exit 1; }
export DEVELOPER_DIR="$DEV"
echo "Using SDK: $SDK"
# Clean build output so there are no stale slices
if [[ "$CLEAN" == "1" ]]; then
echo "Cleaning: rm -rf '$OUT'"
rm -rf "$OUT"
fi
mkdir -p "$OUT/i386" "$OUT/x86_64" "$OUT/universal/$PLUG/Contents/MacOS"
# Arch toggles
# Default: skip i386 on 10.14 SDK; allow override with DO_I386=0/1
if [[ -z "${DO_I386}" ]]; then
if [[ "$SDK" == *"10.14.sdk"* ]]; then
DO_I386=0
else
DO_I386=1
fi
fi
# Minimum OS (override with DEPLOY=10.10, etc.)
DEPLOY="${DEPLOY:-10.14}"
COMMON_CFLAGS=(
-bundle -fobjc-arc -fobjc-link-runtime
-isysroot "$SDK"
-mmacosx-version-min="$DEPLOY"
-I .
)
COMMON_LIBS=(
-framework Foundation
-framework Quartz
-framework AppKit
)
# ---- compile (track what we actually built) ----
BUILT_I386=0
BUILT_X64=0
if [[ "$DO_I386" == "1" ]]; then
echo "Compiling i386…"
clang -arch i386 "${COMMON_CFLAGS[@]}" "${SRCS[@]}" "${COMMON_LIBS[@]}" -o "$OUT/i386/$NAME" || {
echo "i386 build failed (likely unsupported by this SDK/clang). Set DO_I386=0 to skip."
exit 1
}
BUILT_I386=1
else
echo "Skipping i386 (set DO_I386=1 to attempt, e.g. with 10.13 SDK)."
fi
if [[ "$DO_X64" == "1" ]]; then
echo "Compiling x86_64…"
clang -arch x86_64 "${COMMON_CFLAGS[@]}" "${SRCS[@]}" "${COMMON_LIBS[@]}" -o "$OUT/x86_64/$NAME"
BUILT_X64=1
else
echo "Skipping x86_64 (DO_X64=0)."
fi
# ---- gather only slices from this run ----
BINARIES=()
[[ "$BUILT_I386" == "1" && -f "$OUT/i386/$NAME" ]] && BINARIES+=("$OUT/i386/$NAME")
[[ "$BUILT_X64" == "1" && -f "$OUT/x86_64/$NAME" ]] && BINARIES+=("$OUT/x86_64/$NAME")
if [[ "${#BINARIES[@]}" -eq 0 ]]; then
echo "No binaries were built. Aborting."
exit 1
fi
echo "Creating bundle…"
mkdir -p "$OUT/universal/$PLUG/Contents/MacOS"
lipo -create "${BINARIES[@]}" -output "$OUT/universal/$PLUG/Contents/MacOS/$NAME"
# ---- Info.plist ----
cat >"$OUT/universal/$PLUG/Contents/Info.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleDevelopmentRegion</key> <string>English</string>
<key>CFBundleExecutable</key> <string>${NAME}</string>
<key>CFBundleIdentifier</key> <string>com.yourdomain.${NAME}</string>
<key>CFBundleInfoDictionaryVersion</key> <string>6.0</string>
<key>CFBundleName</key> <string>${NAME}</string>
<key>CFBundlePackageType</key> <string>BNDL</string>
<key>CFBundleShortVersionString</key> <string>1.0</string>
<key>CFBundleSupportedPlatforms</key> <array><string>MacOSX</string></array>
<key>CFBundleVersion</key> <string>1</string>
<key>QCPlugInClasses</key>
<array>
<string>${CLASS}</string>
</array>
<key>NSPrincipalClass</key> <string>QCPlugIn</string>
<key>NSRequiresAquaSystemAppearance</key> <false/>
</dict></plist>
PLIST
echo "Final binary slices:"
lipo -info "$OUT/universal/$PLUG/Contents/MacOS/$NAME" || true
echo "Signing…"
codesign --force -s - "$OUT/universal/$PLUG" >/dev/null || true
echo "Installing to: $INST"
mkdir -p "$INST"
rsync -a "$OUT/universal/$PLUG" "$INST/"
echo "Installed: $INST/$PLUG"
echo "Restart Quartz Composer to load the new plug-in."
echo
echo "Notes:"
echo " • CLEAN=$CLEAN (set CLEAN=0 to keep previous builds)."
echo " • i386 default depends on SDK (10.14 → OFF unless DO_I386=1)."
echo " • Override with DO_I386=0/1, DO_X64=0/1."
echo " • DEPLOY=${DEPLOY} (override with DEPLOY=10.10, etc.)."
//
// ISFInputMapperPlugIn.h
// ISF Input Mapper
//
#import <Quartz/Quartz.h>
@interface ISFInputMapperPlugIn : QCPlugIn
// ISF path input
@property (copy) NSString *inputISFPath;
// 17 modulation inputs
@property double inputP1;
@property double inputP2;
@property double inputP3;
@property double inputP4;
@property double inputP5;
@property double inputP6;
@property double inputP7;
@property double inputP8;
@property double inputP9;
@property double inputP10;
@property double inputP11;
@property double inputP12;
@property double inputP13;
@property double inputP14;
@property double inputP15;
@property double inputP16;
@property double inputP17;
// How many numeric params the shader actually has
@property double outputParamCount;
// ISF path output (standardized path if the file exists)
@property (copy) NSString *outputISFPath;
// NOTE: no property-based outputP1..outputP17 any more; they become dynamic ports.
// NOTE: the structure output will also be a dynamic port (no @property here).
@end
//
// ISFInputMapperPlugIn.m
// Quartz Composer ISF Input Mapper
//
// Reads an ISF fragment shader file (.fs), parses its JSON header
// to obtain INPUT definitions, and maps up to 17 numeric INPUTs
// (float/event/int/long/bool) to 17 QC number/boolean outputs.
//
// STATIC PORT BEHAVIOR (to match ISFRenderer static mode):
//
// - We still build an ordered list of numeric INPUTs (float/event/int/long/bool).
// - Ports are laid out in a fixed way, independent of shader order:
//
// P1..P3 → reserved for boolean INPUTs
// (first three boolean shader params in INPUTS order).
// Extra booleans stay at ISF DEFAULT, unmapped to ports.
//
// P4..P17 → all other numeric INPUTs (non-bool) in INPUTS order.
//
// - On ISF path / file change:
// * Reloads INPUT metadata.
// * Builds a list of numeric INPUTs.
// * Builds a static mapping: port → numeric INPUT index.
// * Stores ISF DEFAULT for each mapped port into _defaultValues[i].
// * Marks baselines invalid (unless restored from sticky cache).
//
// - On the FIRST execute after a change (when baselines are invalid):
// * For each P1..P17, snapshot baselineInputs[i] = current QC port value.
// * (unless bypassFirstFrameDefaults is ON) output = ISF DEFAULT.
// * Otherwise, first frame outputs the current port value.
//
// - On subsequent executes:
// * If port i has a mapped numeric INPUT index:
// - If bypassModulation is OFF:
// delta = currentPort[i] - baselineInputs[i]
// value = defaultValues[i] + delta
// - If bypassModulation is ON:
// value = currentPort[i]
// - Optional MIN/MAX clamp (if bypassMinMax is OFF)
// - Type coercion for bool/int/long
// else:
// output = 0.0
//
// STRUCTURE MODE:
//
// - Settings tab has a checkbox "Output as structure (no fixed ports)".
// - When ON:
// * Dynamic numeric ports outputP1..outputP17 are removed.
// * A structure output "Params" (key: outputParams) is added.
// * The structure has one entry per numeric INPUT in the shader
// (not capped at 17), keyed by ISF NAME, with the computed value
// (defaults + modulation via any mapped ports).
// * Unmapped numeric params still appear, at their ISF DEFAULT.
//
// PER-SHADER STICKY VALUES (original mode):
//
// - Settings tab has a checkbox "Per-shader sticky values (remember state per ISF)".
// - When ON:
// * Whenever we switch away from a shader (ISF path/modDate), we
// save its numeric state (defaults, baselines, mapping, baselinesValid).
// * When switching back to that same shader (same path + modDate),
// we restore that cached state instead of re-initializing baselines.
// * First time we see a shader, behaviour is the same as before
// (honouring bypassFirstFrameDefaults, etc.).
//
// STRONGER PER-SHADER STICKY PARAM OUTPUTS (NEW):
//
// - Settings tab now has an additional checkbox
// "Per-shader sticky param outputs (stronger mode)".
// - When ON:
// * For each shader (path + modDate), we remember the *effective
// numeric value* of every ISF numeric INPUT (bool/int/float/etc.)
// as actually sent to the shader.
// * When we switch back to a shader that has cached param outputs,
// on the first frame for that shader we restore those values,
// ignoring the current global QC slider/toggle values for that
// frame. This prevents boolean toggles from “following” you
// between shaders.
// * After that first frame, modulation continues normally using
// the QC ports (sliders/toggles) as before.
//
#import "ISFInputMapperPlugIn.h"
#import "ISFInputMapperPlugInViewController.h"
#import <Foundation/Foundation.h>
#import <Quartz/Quartz.h>
#define kQCPlugIn_Name @"ISF Input Mapper"
#define kQCPlugIn_Description @"Reads ISF INPUT metadata and maps up to 17 numeric inputs to outputs relative to shader defaults."
// Numeric ISF types we support
static BOOL ISFInputTypeIsNumeric(NSString *type)
{
if (![type isKindOfClass:[NSString class]])
return NO;
return ([type isEqualToString:@"float"] ||
[type isEqualToString:@"event"] ||
[type isEqualToString:@"long"] ||
[type isEqualToString:@"int"] ||
[type isEqualToString:@"bool"]);
}
// Number of boolean slots to reserve at the top of the static port layout (P1..P3)
static const NSUInteger kISFStaticMaxBoolPorts = 3;
// Total number of modulation ports (P1..P17)
enum { kISFStaticMaxPorts = 17 };
// Keys for modulation inputs / outputs
static NSString * const kInKeys[kISFStaticMaxPorts] = {
@"inputP1", @"inputP2", @"inputP3", @"inputP4", @"inputP5",
@"inputP6", @"inputP7", @"inputP8", @"inputP9", @"inputP10",
@"inputP11", @"inputP12", @"inputP13", @"inputP14", @"inputP15",
@"inputP16", @"inputP17"
};
static NSString * const kOutKeys[kISFStaticMaxPorts] = {
@"outputP1", @"outputP2", @"outputP3", @"outputP4", @"outputP5",
@"outputP6", @"outputP7", @"outputP8", @"outputP9", @"outputP10",
@"outputP11", @"outputP12", @"outputP13", @"outputP14", @"outputP15",
@"outputP16", @"outputP17"
};
// Structure output key
static NSString * const kStructOutKey = @"outputParams";
#pragma mark - Private settings extension
@interface ISFInputMapperPlugIn ()
// Settings tab (NOT a QC port, internal only)
@property (nonatomic, assign) BOOL useStructureOutput;
// When ON, bypass the default+delta modulation and just use the
// current QC input port value as the base (still subject to type
// rounding and optional MIN/MAX clamp).
@property (nonatomic, assign) BOOL bypassModulation;
// When ON, do not force ISF DEFAULT values to outputs on the first
// frame after loading a shader. Baselines are still captured and
// MIN/MAX metadata is still respected. On the first frame, the
// output will instead use the current QC input (still clamped unless
// bypassMinMax is also ON).
@property (nonatomic, assign) BOOL bypassFirstFrameDefaults;
// When ON, skip clamping values to ISF MIN/MAX.
@property (nonatomic, assign) BOOL bypassMinMax;
// When ON, remember numeric state (defaults + baselines + mapping)
// per shader (path + modDate), and restore it when returning to the
// same shader.
@property (nonatomic, assign) BOOL perShaderStickyValues;
// NEW: when ON, remember per-shader effective numeric parameter
// outputs (one value per numeric INPUT) and restore them on the
// first frame after switching back to that shader. This prevents
// global toggles (e.g. boolean ports) from affecting other shaders.
@property (nonatomic, assign) BOOL perShaderStickyParams;
@end
@implementation ISFInputMapperPlugIn
{
// Raw INPUT dicts from ISF JSON
NSArray *_isfInputs;
// Only numeric INPUTs, in order
NSArray<NSDictionary *> *_numericInputs;
// Which ISF file we're currently using
NSString *_currentISFPath;
NSDate *_currentISFModDate;
// Last shader state key we executed for sticky-param purposes
NSString *_lastStateKeyForParams;
// Per-port state (first kISFStaticMaxPorts only)
double _defaultValues[kISFStaticMaxPorts]; // ISF DEFAULT for the numeric param mapped to that port
double _baselineInputs[kISFStaticMaxPorts]; // snapshot of port at first execute
// For each QC port (P1..P17), which numeric ISF INPUT index it drives.
// -1 means "no shader param mapped to this port".
NSInteger _numericIndexForPort[kISFStaticMaxPorts];
// Whether baselines correspond to the current ISF
BOOL _baselinesValid;
// Settings flag backing ivars
BOOL _useStructureOutput;
BOOL _bypassModulation;
BOOL _bypassFirstFrameDefaults;
BOOL _bypassMinMax;
BOOL _perShaderStickyValues;
BOOL _perShaderStickyParams;
// Per-shader state cache (keyed by path + modDate)
NSMutableDictionary<NSString *, NSDictionary *> *_shaderStateCache;
// NEW: Per-shader *effective* numeric parameter values cache.
// Key: stateKey (path + modDate)
// Value: NSArray<NSNumber *> with one entry per numeric INPUT.
NSMutableDictionary<NSString *, NSArray<NSNumber *> *> *_shaderParamValueCache;
}
@dynamic inputISFPath;
@dynamic inputP1, inputP2, inputP3, inputP4, inputP5;
@dynamic inputP6, inputP7, inputP8, inputP9, inputP10;
@dynamic inputP11, inputP12, inputP13, inputP14, inputP15;
@dynamic inputP16, inputP17;
@dynamic outputParamCount;
@dynamic outputISFPath;
// internal settings (not ports)
@synthesize useStructureOutput = _useStructureOutput;
@synthesize bypassModulation = _bypassModulation;
@synthesize bypassFirstFrameDefaults = _bypassFirstFrameDefaults;
@synthesize bypassMinMax = _bypassMinMax;
@synthesize perShaderStickyValues = _perShaderStickyValues;
@synthesize perShaderStickyParams = _perShaderStickyParams;
#pragma mark - QC Plug-in metadata
+ (NSDictionary *)attributes
{
return @{
QCPlugInAttributeNameKey : kQCPlugIn_Name,
QCPlugInAttributeDescriptionKey : kQCPlugIn_Description
};
}
+ (QCPlugInExecutionMode)executionMode
{
// pure processor
return kQCPlugInExecutionModeProcessor;
}
+ (QCPlugInTimeMode)timeMode
{
return kQCPlugInTimeModeIdle;
}
// Order of property-based ports (inputs + outputs)
+ (NSArray *)sortedPropertyPortKeys
{
return @[
@"inputISFPath",
@"inputP1", @"inputP2", @"inputP3", @"inputP4", @"inputP5",
@"inputP6", @"inputP7", @"inputP8", @"inputP9", @"inputP10",
@"inputP11", @"inputP12", @"inputP13", @"inputP14", @"inputP15",
@"inputP16", @"inputP17",
@"outputParamCount",
@"outputISFPath"
];
}
// Internal settings serialized with the composition but not exposed as ports
+ (NSArray *)plugInKeys
{
NSArray *superKeys = [super plugInKeys];
NSArray *myKeys = @[
@"useStructureOutput",
@"bypassModulation",
@"bypassFirstFrameDefaults",
@"bypassMinMax",
@"perShaderStickyValues",
@"perShaderStickyParams" // NEW
];
return superKeys ? [superKeys arrayByAddingObjectsFromArray:myKeys] : myKeys;
}
#pragma mark - Port attributes (property-based ports only)
+ (NSDictionary *)attributesForPropertyPortWithKey:(NSString *)key
{
// ISF path input
if ([key isEqualToString:@"inputISFPath"]) {
return @{
QCPortAttributeNameKey : @"ISF Path",
QCPortAttributeTypeKey : QCPortTypeString,
QCPortAttributeDefaultValueKey : @""
};
}
// Modulation inputs P1..P17
if ([key hasPrefix:@"inputP"]) {
NSString *display = [key stringByReplacingOccurrencesOfString:@"input"
withString:@""];
NSMutableDictionary *attrs = [NSMutableDictionary dictionary];
attrs[QCPortAttributeNameKey] = display;
BOOL isBoolPort =
[key isEqualToString:@"inputP1"] ||
[key isEqualToString:@"inputP2"] ||
[key isEqualToString:@"inputP3"];
if (isBoolPort) {
// First three ports act as boolean toggles (P1..P3).
attrs[QCPortAttributeTypeKey] = QCPortTypeBoolean;
attrs[QCPortAttributeDefaultValueKey] = @0.0; // off
} else {
// Remaining ports are numeric sliders (P4..P17).
attrs[QCPortAttributeTypeKey] = QCPortTypeNumber;
// 0.5 is just a nice center; it does *not* shift the shader default by itself.
attrs[QCPortAttributeDefaultValueKey] = @0.5;
}
return attrs;
}
// Outputs: Param Count
if ([key isEqualToString:@"outputParamCount"]) {
return @{
QCPortAttributeNameKey : @"Param Count"
};
}
// Output ISF Path (string output)
if ([key isEqualToString:@"outputISFPath"]) {
return @{
QCPortAttributeNameKey : @"ISF Path"
};
}
// NOTE:
// outputP1..outputP17 are dynamic ports now and are NOT handled here.
// The structure output (outputParams) is also dynamic.
return [super attributesForPropertyPortWithKey:key];
}
#pragma mark - Settings view controller
- (QCPlugInViewController *)createViewController
{
// Settings tab UI: structure output + modulation behaviour + sticky state
return [[ISFInputMapperPlugInViewController alloc] initWithPlugIn:self
viewNibName:nil];
}
#pragma mark - Lifecycle
- (id)init
{
self = [super init];
if (self) {
_isfInputs = nil;
_numericInputs = nil;
_currentISFPath = nil;
_currentISFModDate = nil;
_lastStateKeyForParams = nil;
_baselinesValid = NO;
for (int i = 0; i < kISFStaticMaxPorts; ++i) {
_defaultValues[i] = 0.0;
_baselineInputs[i] = 0.0;
_numericIndexForPort[i] = -1;
}
// Default: classic 17 numeric outputs + classic modulation behaviour
_useStructureOutput = NO;
_bypassModulation = NO;
_bypassFirstFrameDefaults = NO;
_bypassMinMax = NO;
_perShaderStickyValues = NO;
_perShaderStickyParams = NO;
_shaderStateCache = [NSMutableDictionary dictionary];
_shaderParamValueCache = [NSMutableDictionary dictionary];
// Set up initial dynamic ports layout
[self _createStaticOutputPorts];
// No structure port by default
}
return self;
}
- (void)_resetState
{
_isfInputs = nil;
_numericInputs = nil;
_currentISFPath = nil;
_currentISFModDate = nil;
_lastStateKeyForParams = nil;
_baselinesValid = NO;
for (int i = 0; i < kISFStaticMaxPorts; ++i) {
_defaultValues[i] = 0.0;
_baselineInputs[i] = 0.0;
_numericIndexForPort[i] = -1;
}
// NOTE: we do NOT touch dynamic ports, settings, or the sticky caches here.
}
// Helper: state key for sticky cache (path + modDate)
- (NSString *)_stateKeyForPath:(NSString *)path modDate:(NSDate *)modDate
{
if (!path) {
path = @"";
}
NSTimeInterval t = modDate ? [modDate timeIntervalSince1970] : 0.0;
return [NSString stringWithFormat:@"%@|%.0f", path, t];
}
- (BOOL)startExecution:(id<QCPlugInContext>)context
{
(void)context;
[self _resetState];
return YES;
}
- (void)stopExecution:(id<QCPlugInContext>)context
{
(void)context;
[self _resetState];
}
#pragma mark - Dynamic outputs (static ports vs structure)
- (void)_safeRemoveOutputPortForKey:(NSString *)key
{
@try {
[self removeOutputPortForKey:key];
} @catch (__unused NSException *ex) {
// Ignore protected/missing ports
}
}
// Create numeric outputs P1..P17 as dynamic ports
- (void)_createStaticOutputPorts
{
for (int i = 0; i < kISFStaticMaxPorts; ++i) {
NSString *key = kOutKeys[i]; // "outputP1".."outputP17"
NSString *display = [key stringByReplacingOccurrencesOfString:@"output"
withString:@""];
NSDictionary *attrs = @{
QCPortAttributeNameKey : display
};
[self addOutputPortWithType:QCPortTypeNumber
forKey:key
withAttributes:attrs];
}
}
- (void)_destroyStaticOutputPorts
{
for (int i = 0; i < kISFStaticMaxPorts; ++i) {
[self _safeRemoveOutputPortForKey:kOutKeys[i]];
}
}
// Ensure structure output exists and is at the bottom
- (void)_ensureStructureOutputPort
{
[self _safeRemoveOutputPortForKey:kStructOutKey];
[self addOutputPortWithType:QCPortTypeStructure
forKey:kStructOutKey
withAttributes:@{ QCPortAttributeNameKey : @"Params" }];
}
- (void)_destroyStructureOutputPort
{
[self _safeRemoveOutputPortForKey:kStructOutKey];
}
// KVC setter for settings tab
- (void)setUseStructureOutput:(BOOL)useStructureOutput
{
if (_useStructureOutput == useStructureOutput)
return;
_useStructureOutput = useStructureOutput;
if (_useStructureOutput) {
// Structure mode: remove P1..P17 outputs, add structure
[self _destroyStaticOutputPorts];
[self _ensureStructureOutputPort];
} else {
// Static mode: remove structure, restore P1..P17 outputs
[self _destroyStructureOutputPort];
[self _createStaticOutputPorts];
}
}
// Optional: when enabling strong sticky params, reset last-state key so
// the next execute treats the current shader as "new" for this mode.
- (void)setPerShaderStickyParams:(BOOL)perShaderStickyParams
{
if (_perShaderStickyParams == perShaderStickyParams)
return;
_perShaderStickyParams = perShaderStickyParams;
_lastStateKeyForParams = nil;
}
#pragma mark - ISF header parsing
// Parse JSON header from /* ... */ block at top of file.
- (NSArray *)_parseISFInputsFromFragmentSource:(NSString *)fragSource
{
if (!fragSource || fragSource.length == 0)
return nil;
NSMutableArray *parsedInputs = [NSMutableArray array];
@try {
NSRange commentStart = [fragSource rangeOfString:@"/*"];
if (commentStart.location == NSNotFound)
return nil;
NSRange searchRange = NSMakeRange(commentStart.location + 2,
fragSource.length - (commentStart.location + 2));
NSRange commentEnd = [fragSource rangeOfString:@"*/"
options:0
range:searchRange];
if (commentEnd.location == NSNotFound)
return nil;
NSUInteger innerStart = commentStart.location + 2;
NSUInteger innerLen = commentEnd.location - innerStart;
if (innerStart + innerLen > fragSource.length)
return nil;
NSString *commentBody =
[fragSource substringWithRange:NSMakeRange(innerStart, innerLen)];
NSRange braceStart = [commentBody rangeOfString:@"{"];
NSRange braceEnd = [commentBody rangeOfString:@"}"
options:NSBackwardsSearch];
if (braceStart.location == NSNotFound ||
braceEnd.location == NSNotFound) {
return nil;
}
NSUInteger jsonStart = braceStart.location;
NSUInteger jsonLen = braceEnd.location - jsonStart + 1;
if (jsonStart + jsonLen > commentBody.length)
return nil;
NSString *jsonString =
[commentBody substringWithRange:NSMakeRange(jsonStart, jsonLen)];
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
if (!jsonData)
return nil;
NSError *jsonErr = nil;
id jsonObj = [NSJSONSerialization JSONObjectWithData:jsonData
options:0
error:&jsonErr];
if (jsonErr || ![jsonObj isKindOfClass:[NSDictionary class]]) {
NSLog(@"[ISFInputMapper] JSON parse error: %@", jsonErr);
return nil;
}
NSDictionary *root = (NSDictionary *)jsonObj;
id inputsObj = root[@"INPUTS"];
if (![inputsObj isKindOfClass:[NSArray class]])
return nil;
for (id inp in (NSArray *)inputsObj) {
if (![inp isKindOfClass:[NSDictionary class]])
continue;
NSDictionary *inputDict = (NSDictionary *)inp;
NSString *name = inputDict[@"NAME"];
NSString *type = inputDict[@"TYPE"];
if (![name isKindOfClass:[NSString class]] ||
![type isKindOfClass:[NSString class]] ||
name.length == 0)
continue;
[parsedInputs addObject:inputDict];
}
}
@catch (NSException *ex) {
NSLog(@"[ISFInputMapper] Exception while parsing ISF header: %@", ex);
return nil;
}
return (parsedInputs.count > 0) ? [parsedInputs copy] : nil;
}
// Extract numeric DEFAULT from an ISF INPUT dict.
- (double)_defaultValueForNumericInput:(NSDictionary *)input
{
NSString *type = input[@"TYPE"];
id defObj = input[@"DEFAULT"];
if ([type isEqualToString:@"bool"]) {
if ([defObj respondsToSelector:@selector(boolValue)]) {
return [defObj boolValue] ? 1.0 : 0.0;
}
return 0.0;
}
if ([defObj respondsToSelector:@selector(doubleValue)]) {
return [defObj doubleValue];
}
return 0.0;
}
#pragma mark - Static port mapping (bool-first)
// Build static "bool-first" mapping from numeric ISF INPUTs to QC ports (P1..P17).
// - Reserve P1..P3 for boolean INPUTs in INPUTS order.
// - Extra booleans beyond three are unmapped (shader uses DEFAULT).
// - Non-bool numeric INPUTs go to P4..P17 sequentially in INPUTS order.
// - Ports with no mapped param just output 0.0 and do not modulate anything.
- (void)_rebuildNumericPortMapping
{
// Clear mapping + defaults
for (int i = 0; i < kISFStaticMaxPorts; ++i) {
_numericIndexForPort[i] = -1;
_defaultValues[i] = 0.0;
}
if (!_numericInputs || _numericInputs.count == 0) {
return;
}
NSUInteger boolSlotsUsed = 0; // how many of P1..P3 are occupied
NSUInteger nonBoolStaticIndex = kISFStaticMaxBoolPorts; // first non-bool goes to P4
NSUInteger numericCount = _numericInputs.count;
for (NSUInteger paramIndex = 0; paramIndex < numericCount; ++paramIndex) {
NSDictionary *inputDesc = _numericInputs[paramIndex];
NSString *type = inputDesc[@"TYPE"];
if (![type isKindOfClass:[NSString class]]) {
continue;
}
BOOL isBool = [type isEqualToString:@"bool"];
NSUInteger portIndex = 0;
if (isBool) {
// Map first kISFStaticMaxBoolPorts booleans to P1..P3.
if (boolSlotsUsed >= kISFStaticMaxBoolPorts) {
// More booleans than slots: leave unmapped (use DEFAULT only).
continue;
}
portIndex = boolSlotsUsed; // 0→P1, 1→P2, 2→P3
boolSlotsUsed++;
} else {
// Non-bool numeric params go to P4..P17.
portIndex = nonBoolStaticIndex;
nonBoolStaticIndex++;
}
if (portIndex >= kISFStaticMaxPorts) {
// No more physical ports available.
continue;
}
_numericIndexForPort[portIndex] = (NSInteger)paramIndex;
_defaultValues[portIndex] = [self _defaultValueForNumericInput:inputDesc];
}
}
#pragma mark - ISF reload with per-shader sticky cache
// Reloads ISF inputs if path or file mod time changed.
// When perShaderStickyValues is ON, we cache numeric state per
// (path + modDate) when leaving a shader, and restore it when
// returning to that same shader.
- (void)_reloadISFInputsIfNeeded
{
NSString *path = self.inputISFPath ?: @"";
if (path.length == 0) {
[self _resetState];
return;
}
NSString *stdPath = [path stringByStandardizingPath];
NSFileManager *fm = [NSFileManager defaultManager];
BOOL isDir = NO;
if (![fm fileExistsAtPath:stdPath isDirectory:&isDir] || isDir) {
if (!_currentISFPath || ![_currentISFPath isEqualToString:stdPath]) {
NSLog(@"[ISFInputMapper] ISF path '%@' does not exist or is a directory", stdPath);
}
[self _resetState];
return;
}
NSError *attrErr = nil;
NSDictionary *attrs = [fm attributesOfItemAtPath:stdPath error:&attrErr];
if (!attrs) {
NSLog(@"[ISFInputMapper] Could not stat ISF at '%@': %@", stdPath, attrErr);
[self _resetState];
return;
}
NSDate *modDate = attrs.fileModificationDate;
BOOL pathChanged = (!_currentISFPath || ![_currentISFPath isEqualToString:stdPath]);
BOOL dateChanged = (!_currentISFModDate || ![_currentISFModDate isEqualToDate:modDate]);
if (!pathChanged && !dateChanged && _isfInputs) {
// No change → keep current inputs/defaults/baseline state.
return;
}
BOOL sticky = self.perShaderStickyValues;
// If we're switching away from a valid current shader and sticky
// is ON, cache its numeric state keyed by (oldPath + oldModDate).
if (sticky && _currentISFPath && _currentISFModDate && _isfInputs) {
NSString *oldKey = [self _stateKeyForPath:_currentISFPath
modDate:_currentISFModDate];
NSMutableDictionary *state = [NSMutableDictionary dictionary];
state[@"isfInputs"] = _isfInputs ?: @[];
state[@"numericInputs"] = _numericInputs ?: @[];
NSMutableArray *defaults = [NSMutableArray arrayWithCapacity:kISFStaticMaxPorts];
NSMutableArray *baselines = [NSMutableArray arrayWithCapacity:kISFStaticMaxPorts];
NSMutableArray *mapping = [NSMutableArray arrayWithCapacity:kISFStaticMaxPorts];
for (int i = 0; i < kISFStaticMaxPorts; ++i) {
[defaults addObject:@(_defaultValues[i])];
[baselines addObject:@(_baselineInputs[i])];
[mapping addObject:@(_numericIndexForPort[i])];
}
state[@"defaults"] = defaults;
state[@"baselines"] = baselines;
state[@"mapping"] = mapping;
state[@"baselinesValid"] = @(_baselinesValid);
if (!_shaderStateCache) {
_shaderStateCache = [NSMutableDictionary dictionary];
}
_shaderStateCache[oldKey] = state;
}
NSString *newKey = sticky ? [self _stateKeyForPath:stdPath modDate:modDate] : nil;
// If sticky is ON and we have cached state for this shader, restore it.
if (sticky && _shaderStateCache && newKey) {
NSDictionary *cached = _shaderStateCache[newKey];
if (cached) {
_currentISFPath = stdPath;
_currentISFModDate = modDate;
_isfInputs = cached[@"isfInputs"];
_numericInputs = cached[@"numericInputs"];
NSArray *defaults = cached[@"defaults"];
NSArray *baselines = cached[@"baselines"];
NSArray *mapping = cached[@"mapping"];
if (defaults.count == kISFStaticMaxPorts &&
baselines.count == kISFStaticMaxPorts &&
mapping.count == kISFStaticMaxPorts &&
_isfInputs && _numericInputs) {
for (int i = 0; i < kISFStaticMaxPorts; ++i) {
_defaultValues[i] = [defaults[i] doubleValue];
_baselineInputs[i] = [baselines[i] doubleValue];
_numericIndexForPort[i] = [mapping[i] integerValue];
}
_baselinesValid = [cached[@"baselinesValid"] boolValue];
NSLog(@"[ISFInputMapper] Restored cached state for ISF '%@'.", stdPath);
return;
}
// If cached structure is malformed, fall through to fresh parse.
NSLog(@"[ISFInputMapper] Cached state for '%@' invalid; reparsing.", stdPath);
}
}
// Fresh parse of the shader (first time, or sticky OFF, or no valid cache).
_currentISFPath = stdPath;
_currentISFModDate = modDate;
_isfInputs = nil;
_numericInputs = nil;
_baselinesValid = NO;
_lastStateKeyForParams = nil;
for (int i = 0; i < kISFStaticMaxPorts; ++i) {
_defaultValues[i] = 0.0;
_baselineInputs[i] = 0.0;
_numericIndexForPort[i] = -1;
}
NSError *readErr = nil;
NSString *fragSource =
[NSString stringWithContentsOfFile:stdPath
encoding:NSUTF8StringEncoding
error:&readErr];
if (!fragSource) {
NSLog(@"[ISFInputMapper] Failed to read ISF at '%@': %@", stdPath, readErr);
[self _resetState];
return;
}
NSArray *inputs = [self _parseISFInputsFromFragmentSource:fragSource];
if (!inputs) {
NSLog(@"[ISFInputMapper] No INPUT entries found in ISF '%@'", stdPath);
[self _resetState];
return;
}
_isfInputs = inputs;
// Build numeric list (bool/float/event/int/long only).
NSMutableArray<NSDictionary *> *numeric = [NSMutableArray array];
for (NSDictionary *input in inputs) {
NSString *type = input[@"TYPE"];
if (ISFInputTypeIsNumeric(type)) {
[numeric addObject:input];
}
}
_numericInputs = [numeric copy];
// Build static "bool-first" mapping P1..P17 -> numeric INPUT indices
[self _rebuildNumericPortMapping];
NSLog(@"[ISFInputMapper] Loaded %lu INPUT(s), %lu numeric, from '%@'. Baselines invalidated.",
(unsigned long)_isfInputs.count,
(unsigned long)_numericInputs.count,
stdPath);
}
#pragma mark - Execution
- (BOOL)execute:(id<QCPlugInContext>)context
atTime:(NSTimeInterval)time
withArguments:(NSDictionary *)arguments
{
(void)context;
(void)time;
(void)arguments;
[self _reloadISFInputsIfNeeded];
// Keep an up-to-date output ISF path (standardized if file exists)
NSString *pathOut = _currentISFPath ?: (self.inputISFPath ?: @"");
self.outputISFPath = pathOut;
BOOL structMode = self.useStructureOutput;
BOOL bypassModulation = self.bypassModulation;
BOOL bypassFirstFrameDefaults = self.bypassFirstFrameDefaults;
BOOL bypassMinMax = self.bypassMinMax;
BOOL stickyParamsMode = self.perShaderStickyParams;
if (!_numericInputs) {
// Either no ISF or no numeric inputs
self.outputParamCount = 0.0;
if (!structMode) {
for (int i = 0; i < kISFStaticMaxPorts; ++i) {
@try {
[self setValue:@0.0 forOutputKey:kOutKeys[i]];
}
@catch (__unused NSException *ex) {
}
}
} else {
// Empty structure
@try {
[self setValue:@{} forOutputKey:kStructOutKey];
}
@catch (__unused NSException *ex) {
}
}
return YES;
}
NSUInteger numericCount = _numericInputs.count;
// "How many numeric params the shader actually has" (even if some are unmapped)
self.outputParamCount = (double)numericCount;
// For strong per-shader sticky params, compute a state key and
// determine if we've just switched shaders for this mode.
NSString *stateKey = nil;
NSArray<NSNumber *> *cachedParamValues = nil;
BOOL haveCachedParamValues = NO;
BOOL newShaderForStickyParams = NO;
if (stickyParamsMode && _currentISFPath && _currentISFModDate) {
stateKey = [self _stateKeyForPath:_currentISFPath modDate:_currentISFModDate];
if (stateKey) {
newShaderForStickyParams =
(!_lastStateKeyForParams ||
![_lastStateKeyForParams isEqualToString:stateKey]);
if (newShaderForStickyParams) {
cachedParamValues = _shaderParamValueCache[stateKey];
if (cachedParamValues && cachedParamValues.count == numericCount) {
haveCachedParamValues = YES;
}
}
}
}
// Baseline capture is still controlled by _baselinesValid, as before.
BOOL capturingBaselinesThisFrame = !_baselinesValid;
// Per-port computed values (for both static outputs and structure building)
double portValues[kISFStaticMaxPorts];
for (int i = 0; i < kISFStaticMaxPorts; ++i) {
portValues[i] = 0.0;
}
// Per-port output computation
for (int i = 0; i < kISFStaticMaxPorts; ++i) {
double sliderVal = 0.0;
@try {
id v = [self valueForInputKey:kInKeys[i]];
if ([v respondsToSelector:@selector(doubleValue)]) {
sliderVal = [v doubleValue];
}
}
@catch (__unused NSException *ex) {
}
double outVal = 0.0;
// Which numeric ISF INPUT (if any) does this port drive?
NSInteger paramIndex = _numericIndexForPort[i];
// Decide if we should use a cached param value for this param
BOOL useCachedParamValue = NO;
double cachedVal = 0.0;
if (stickyParamsMode &&
newShaderForStickyParams &&
haveCachedParamValues &&
paramIndex >= 0 &&
paramIndex < (NSInteger)numericCount) {
useCachedParamValue = YES;
cachedVal = [cachedParamValues[(NSUInteger)paramIndex] doubleValue];
}
if (paramIndex >= 0 && paramIndex < (NSInteger)numericCount) {
NSDictionary *inputDesc = _numericInputs[(NSUInteger)paramIndex];
NSString *type = inputDesc[@"TYPE"];
id minObj = inputDesc[@"MIN"];
id maxObj = inputDesc[@"MAX"];
BOOL hasMin = [minObj respondsToSelector:@selector(doubleValue)];
BOOL hasMax = [maxObj respondsToSelector:@selector(doubleValue)];
double minVal = hasMin ? [minObj doubleValue] : 0.0;
double maxVal = hasMax ? [maxObj doubleValue] : 0.0;
double baseDefault = _defaultValues[i]; // ISF DEFAULT for this port
// --- Baseline capture (fixed to respect sticky params) ---
if (capturingBaselinesThisFrame) {
if (useCachedParamValue && !bypassModulation) {
// Choose a baseline so that with the *current* slider value,
// DEFAULT + (slider - baseline) == cachedVal.
//
// cachedVal = baseDefault + (sliderVal - baseline)
// => baseline = sliderVal + baseDefault - cachedVal
double baseline = sliderVal + baseDefault - cachedVal;
_baselineInputs[i] = baseline;
} else {
// Normal behaviour: just snapshot the current slider
_baselineInputs[i] = sliderVal;
}
}
double baseSlider = _baselineInputs[i]; // port at first frame (or adjusted above)
double value = 0.0;
// NEW: strong per-shader sticky param outputs.
// If we have cached effective values for this shader and this
// is the first frame we've been on this shader (for this mode),
// then start from the cached value instead of the global slider.
if (useCachedParamValue) {
value = cachedVal;
} else if (bypassModulation) {
// No default+delta modulation; just push current QC input
// (still subject to type and optional MIN/MAX).
value = sliderVal;
} else {
// Original modulation behaviour
if (capturingBaselinesThisFrame && bypassFirstFrameDefaults) {
// First frame: do NOT override shader with ISF DEFAULT;
// just pass through the current QC input (still respecting MIN/MAX).
value = sliderVal;
}
else if (capturingBaselinesThisFrame && !bypassFirstFrameDefaults) {
// Original behaviour: first frame uses ISF DEFAULT, no modulation yet.
value = baseDefault;
}
else {
// Subsequent frames: DEFAULT + delta
double delta = sliderVal - baseSlider;
value = baseDefault + delta;
}
}
// MIN/MAX clamp (unless bypassed)
if (!bypassMinMax) {
if (hasMin && value < minVal) value = minVal;
if (hasMax && value > maxVal) value = maxVal;
}
// Type-specific rounding
if ([type isEqualToString:@"bool"]) {
outVal = (value >= 0.5) ? 1.0 : 0.0;
}
else if ([type isEqualToString:@"long"] ||
[type isEqualToString:@"int"]) {
outVal = (double)llround(value);
}
else {
outVal = value;
}
} else {
// No shader param mapped to this port: just output 0.0
outVal = 0.0;
}
portValues[i] = outVal;
// In static mode, write directly to numeric outputs
if (!structMode) {
@try {
[self setValue:@(outVal) forOutputKey:kOutKeys[i]];
}
@catch (__unused NSException *ex) {
}
}
}
if (capturingBaselinesThisFrame) {
_baselinesValid = YES;
}
// Build paramIndex → portIndex map (first mapped port wins)
NSInteger portIndexForNumeric[numericCount];
for (NSUInteger j = 0; j < numericCount; ++j) {
portIndexForNumeric[j] = -1;
}
for (int i = 0; i < kISFStaticMaxPorts; ++i) {
NSInteger pIdx = _numericIndexForPort[i];
if (pIdx >= 0 && pIdx < (NSInteger)numericCount &&
portIndexForNumeric[pIdx] < 0) {
portIndexForNumeric[pIdx] = i;
}
}
// Effective numeric value for each ISF numeric INPUT (regardless of
// whether we're in structure mode). Used both for struct mode and
// for the strong per-shader sticky param cache.
double paramEffectiveValues[numericCount];
for (NSUInteger paramIndex = 0; paramIndex < numericCount; ++paramIndex) {
NSDictionary *inputDesc = _numericInputs[paramIndex];
NSInteger portIndex = portIndexForNumeric[paramIndex];
double value;
if (portIndex >= 0 && portIndex < kISFStaticMaxPorts) {
// This param is driven by one of the 17 modulation ports
value = portValues[portIndex];
} else {
// No modulation: just use the ISF DEFAULT
value = [self _defaultValueForNumericInput:inputDesc];
}
paramEffectiveValues[paramIndex] = value;
}
// Structure mode: build a structure of all numeric ISF params keyed by NAME
if (structMode) {
NSMutableDictionary *paramStruct =
[NSMutableDictionary dictionaryWithCapacity:numericCount];
for (NSUInteger paramIndex = 0; paramIndex < numericCount; ++paramIndex) {
NSDictionary *inputDesc = _numericInputs[paramIndex];
NSString *name = inputDesc[@"NAME"];
if (![name isKindOfClass:[NSString class]] || name.length == 0) {
continue;
}
double value = paramEffectiveValues[paramIndex];
paramStruct[name] = @(value);
}
@try {
[self setValue:paramStruct forOutputKey:kStructOutKey];
}
@catch (__unused NSException *ex) {
}
}
// Update strong per-shader sticky-param cache with the effective values
// we just computed for this shader.
if (stickyParamsMode && stateKey && numericCount > 0) {
NSMutableArray<NSNumber *> *arr =
[NSMutableArray arrayWithCapacity:numericCount];
for (NSUInteger paramIndex = 0; paramIndex < numericCount; ++paramIndex) {
[arr addObject:@(paramEffectiveValues[paramIndex])];
}
if (!_shaderParamValueCache) {
_shaderParamValueCache = [NSMutableDictionary dictionary];
}
_shaderParamValueCache[stateKey] = [arr copy];
}
// Remember the last state key we executed for sticky-param purposes
_lastStateKeyForParams = stateKey;
return YES;
}
@end
//
// ISFInputMapperPlugInViewController.h
// ISF Input Mapper
//
#import <Quartz/Quartz.h>
#import <AppKit/AppKit.h>
@interface ISFInputMapperPlugInViewController : QCPlugInViewController
{
NSButton *_useStructureCheckbox;
NSButton *_bypassModulationCheckbox;
NSButton *_bypassFirstFrameDefaultsCheckbox;
NSButton *_bypassMinMaxCheckbox;
NSButton *_perShaderStickyCheckbox;
NSButton *_perShaderStickyParamsCheckbox;
}
@property(nonatomic, strong) NSButton *useStructureCheckbox;
@property(nonatomic, strong) NSButton *bypassModulationCheckbox;
@property(nonatomic, strong) NSButton *bypassFirstFrameDefaultsCheckbox;
@property(nonatomic, strong) NSButton *bypassMinMaxCheckbox;
@property(nonatomic, strong) NSButton *perShaderStickyCheckbox;
@property(nonatomic, strong) NSButton *perShaderStickyParamsCheckbox;
@end
//
// ISFInputMapperPlugInViewController.m
// ISF Input Mapper
//
#import "ISFInputMapperPlugInViewController.h"
@implementation ISFInputMapperPlugInViewController
@synthesize useStructureCheckbox = _useStructureCheckbox;
@synthesize bypassModulationCheckbox = _bypassModulationCheckbox;
@synthesize bypassFirstFrameDefaultsCheckbox = _bypassFirstFrameDefaultsCheckbox;
@synthesize bypassMinMaxCheckbox = _bypassMinMaxCheckbox;
@synthesize perShaderStickyCheckbox = _perShaderStickyCheckbox;
@synthesize perShaderStickyParamsCheckbox = _perShaderStickyParamsCheckbox;
- (id)initWithPlugIn:(QCPlugIn *)plugIn viewNibName:(NSString *)nibName
{
self = [super initWithPlugIn:plugIn viewNibName:nibName];
if (self) {
// nothing special
}
return self;
}
- (void)loadView
{
// Root view for the Settings tab
// Slightly taller to fit the extra checkbox
NSView *root = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 360, 210)];
root.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
NSRect bounds = root.bounds;
CGFloat x = 8.0;
CGFloat y = NSMaxY(bounds) - 8.0;
// Checkbox: "Output as structure (no fixed ports)"
NSButton *cb1 = [[NSButton alloc] initWithFrame:NSZeroRect];
[cb1 setButtonType:NSSwitchButton];
cb1.title = @"Output as structure (no fixed ports)";
[cb1 sizeToFit];
NSRect frame1 = cb1.frame;
frame1.origin.x = x;
frame1.origin.y = y - NSHeight(frame1);
cb1.frame = frame1;
cb1.autoresizingMask = NSViewMaxXMargin | NSViewMinYMargin;
[cb1 setTarget:self];
[cb1 setAction:@selector(_useStructureCheckboxChanged:)];
[root addSubview:cb1];
self.useStructureCheckbox = cb1;
y = NSMinY(frame1) - 4.0;
// Checkbox: "Bypass modulation (use input ports directly)"
NSButton *cb2 = [[NSButton alloc] initWithFrame:NSZeroRect];
[cb2 setButtonType:NSSwitchButton];
cb2.title = @"Bypass modulation (use input ports directly)";
[cb2 sizeToFit];
NSRect frame2 = cb2.frame;
frame2.origin.x = x;
frame2.origin.y = y - NSHeight(frame2);
cb2.frame = frame2;
cb2.autoresizingMask = NSViewMaxXMargin | NSViewMinYMargin;
[cb2 setTarget:self];
[cb2 setAction:@selector(_bypassModulationCheckboxChanged:)];
[root addSubview:cb2];
self.bypassModulationCheckbox = cb2;
y = NSMinY(frame2) - 4.0;
// Checkbox: "Bypass ISF defaults on first frame"
NSButton *cb3 = [[NSButton alloc] initWithFrame:NSZeroRect];
[cb3 setButtonType:NSSwitchButton];
cb3.title = @"Bypass ISF defaults on first frame";
[cb3 sizeToFit];
NSRect frame3 = cb3.frame;
frame3.origin.x = x;
frame3.origin.y = y - NSHeight(frame3);
cb3.frame = frame3;
cb3.autoresizingMask = NSViewMaxXMargin | NSViewMinYMargin;
[cb3 setTarget:self];
[cb3 setAction:@selector(_bypassFirstFrameDefaultsCheckboxChanged:)];
[root addSubview:cb3];
self.bypassFirstFrameDefaultsCheckbox = cb3;
y = NSMinY(frame3) - 4.0;
// Checkbox: "Bypass MIN/MAX clamp"
NSButton *cb4 = [[NSButton alloc] initWithFrame:NSZeroRect];
[cb4 setButtonType:NSSwitchButton];
cb4.title = @"Bypass MIN/MAX clamp";
[cb4 sizeToFit];
NSRect frame4 = cb4.frame;
frame4.origin.x = x;
frame4.origin.y = y - NSHeight(frame4);
cb4.frame = frame4;
cb4.autoresizingMask = NSViewMaxXMargin | NSViewMinYMargin;
[cb4 setTarget:self];
[cb4 setAction:@selector(_bypassMinMaxCheckboxChanged:)];
[root addSubview:cb4];
self.bypassMinMaxCheckbox = cb4;
y = NSMinY(frame4) - 4.0;
// Checkbox: "Per-shader sticky values (remember state per ISF)"
NSButton *cb5 = [[NSButton alloc] initWithFrame:NSZeroRect];
[cb5 setButtonType:NSSwitchButton];
cb5.title = @"Per-shader sticky values (remember state per ISF)";
[cb5 sizeToFit];
NSRect frame5 = cb5.frame;
frame5.origin.x = x;
frame5.origin.y = y - NSHeight(frame5);
cb5.frame = frame5;
cb5.autoresizingMask = NSViewMaxXMargin | NSViewMinYMargin;
[cb5 setTarget:self];
[cb5 setAction:@selector(_perShaderStickyCheckboxChanged:)];
[root addSubview:cb5];
self.perShaderStickyCheckbox = cb5;
y = NSMinY(frame5) - 4.0;
// NEW: Checkbox: "Per-shader sticky param outputs (stronger mode)"
NSButton *cb6 = [[NSButton alloc] initWithFrame:NSZeroRect];
[cb6 setButtonType:NSSwitchButton];
cb6.title = @"Per-shader sticky param outputs (stronger mode)";
[cb6 sizeToFit];
NSRect frame6 = cb6.frame;
frame6.origin.x = x;
frame6.origin.y = y - NSHeight(frame6);
cb6.frame = frame6;
cb6.autoresizingMask = NSViewMaxXMargin | NSViewMinYMargin;
[cb6 setTarget:self];
[cb6 setAction:@selector(_perShaderStickyParamsCheckboxChanged:)];
[root addSubview:cb6];
self.perShaderStickyParamsCheckbox = cb6;
self.view = root;
}
- (void)viewWillAppear
{
[super viewWillAppear];
id plugin = self.plugIn;
if (!plugin)
return;
// useStructureOutput
@try {
NSNumber *val = [plugin valueForKey:@"useStructureOutput"];
BOOL on = val ? val.boolValue : NO;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
self.useStructureCheckbox.state = on ? NSControlStateValueOn : NSControlStateValueOff;
#else
self.useStructureCheckbox.state = on ? NSOnState : NSOffState;
#endif
}
@catch (__unused id ex) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
self.useStructureCheckbox.state = NSControlStateValueOff;
#else
self.useStructureCheckbox.state = NSOffState;
#endif
}
// bypassModulation
@try {
NSNumber *val = [plugin valueForKey:@"bypassModulation"];
BOOL on = val ? val.boolValue : NO;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
self.bypassModulationCheckbox.state = on ? NSControlStateValueOn : NSControlStateValueOff;
#else
self.bypassModulationCheckbox.state = on ? NSOnState : NSOffState;
#endif
}
@catch (__unused id ex) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
self.bypassModulationCheckbox.state = NSControlStateValueOff;
#else
self.bypassModulationCheckbox.state = NSOffState;
#endif
}
// bypassFirstFrameDefaults
@try {
NSNumber *val = [plugin valueForKey:@"bypassFirstFrameDefaults"];
BOOL on = val ? val.boolValue : NO;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
self.bypassFirstFrameDefaultsCheckbox.state = on ? NSControlStateValueOn : NSControlStateValueOff;
#else
self.bypassFirstFrameDefaultsCheckbox.state = on ? NSOnState : NSOffState;
#endif
}
@catch (__unused id ex) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
self.bypassFirstFrameDefaultsCheckbox.state = NSControlStateValueOff;
#else
self.bypassFirstFrameDefaultsCheckbox.state = NSOffState;
#endif
}
// bypassMinMax
@try {
NSNumber *val = [plugin valueForKey:@"bypassMinMax"];
BOOL on = val ? val.boolValue : NO;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
self.bypassMinMaxCheckbox.state = on ? NSControlStateValueOn : NSControlStateValueOff;
#else
self.bypassMinMaxCheckbox.state = on ? NSOnState : NSOffState;
#endif
}
@catch (__unused id ex) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
self.bypassMinMaxCheckbox.state = NSControlStateValueOff;
#else
self.bypassMinMaxCheckbox.state = NSOffState;
#endif
}
// perShaderStickyValues
@try {
NSNumber *val = [plugin valueForKey:@"perShaderStickyValues"];
BOOL on = val ? val.boolValue : NO;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
self.perShaderStickyCheckbox.state = on ? NSControlStateValueOn : NSControlStateValueOff;
#else
self.perShaderStickyCheckbox.state = on ? NSOnState : NSOffState;
#endif
}
@catch (__unused id ex) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
self.perShaderStickyCheckbox.state = NSControlStateValueOff;
#else
self.perShaderStickyCheckbox.state = NSOffState;
#endif
}
// NEW: perShaderStickyParams
@try {
NSNumber *val = [plugin valueForKey:@"perShaderStickyParams"];
BOOL on = val ? val.boolValue : NO;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
self.perShaderStickyParamsCheckbox.state = on ? NSControlStateValueOn : NSControlStateValueOff;
#else
self.perShaderStickyParamsCheckbox.state = on ? NSOnState : NSOffState;
#endif
}
@catch (__unused id ex) {
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
self.perShaderStickyParamsCheckbox.state = NSControlStateValueOff;
#else
self.perShaderStickyParamsCheckbox.state = NSOffState;
#endif
}
}
#pragma mark - Actions
- (void)_useStructureCheckboxChanged:(id)sender
{
(void)sender;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
BOOL on = (self.useStructureCheckbox.state == NSControlStateValueOn);
#else
BOOL on = (self.useStructureCheckbox.state == NSOnState);
#endif
id plugin = self.plugIn;
if (plugin) {
@try {
[plugin setValue:@(on) forKey:@"useStructureOutput"];
}
@catch (__unused id ex) {
// ignore KVC failures
}
}
}
- (void)_bypassModulationCheckboxChanged:(id)sender
{
(void)sender;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
BOOL on = (self.bypassModulationCheckbox.state == NSControlStateValueOn);
#else
BOOL on = (self.bypassModulationCheckbox.state == NSOnState);
#endif
id plugin = self.plugIn;
if (plugin) {
@try {
[plugin setValue:@(on) forKey:@"bypassModulation"];
}
@catch (__unused id ex) {
// ignore KVC failures
}
}
}
- (void)_bypassFirstFrameDefaultsCheckboxChanged:(id)sender
{
(void)sender;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
BOOL on = (self.bypassFirstFrameDefaultsCheckbox.state == NSControlStateValueOn);
#else
BOOL on = (self.bypassFirstFrameDefaultsCheckbox.state == NSOnState);
#endif
id plugin = self.plugIn;
if (plugin) {
@try {
[plugin setValue:@(on) forKey:@"bypassFirstFrameDefaults"];
}
@catch (__unused id ex) {
// ignore KVC failures
}
}
}
- (void)_bypassMinMaxCheckboxChanged:(id)sender
{
(void)sender;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
BOOL on = (self.bypassMinMaxCheckbox.state == NSControlStateValueOn);
#else
BOOL on = (self.bypassMinMaxCheckbox.state == NSOnState);
#endif
id plugin = self.plugIn;
if (plugin) {
@try {
[plugin setValue:@(on) forKey:@"bypassMinMax"];
}
@catch (__unused id ex) {
// ignore KVC failures
}
}
}
- (void)_perShaderStickyCheckboxChanged:(id)sender
{
(void)sender;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
BOOL on = (self.perShaderStickyCheckbox.state == NSControlStateValueOn);
#else
BOOL on = (self.perShaderStickyCheckbox.state == NSOnState);
#endif
id plugin = self.plugIn;
if (plugin) {
@try {
[plugin setValue:@(on) forKey:@"perShaderStickyValues"];
}
@catch (__unused id ex) {
// ignore KVC failures
}
}
}
// NEW: strong per-shader sticky param outputs
- (void)_perShaderStickyParamsCheckboxChanged:(id)sender
{
(void)sender;
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101000
BOOL on = (self.perShaderStickyParamsCheckbox.state == NSControlStateValueOn);
#else
BOOL on = (self.perShaderStickyParamsCheckbox.state == NSOnState);
#endif
id plugin = self.plugIn;
if (plugin) {
@try {
[plugin setValue:@(on) forKey:@"perShaderStickyParams"];
}
@catch (__unused id ex) {
// ignore KVC failures
}
}
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment