Last active
November 27, 2025 08:25
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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.)." |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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