davinci-resolve-mcp 2.57.4 → 2.58.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +20 -0
- package/CHANGELOG.md +197 -2
- package/LICENSE +1 -1
- package/README.md +73 -1
- package/bin/davinci-resolve-advanced-mcp.mjs +20 -0
- package/docs/README.md +19 -0
- package/docs/SKILL.md +62 -1
- package/docs/contributing.md +1 -1
- package/docs/guides/control-panel.md +9 -0
- package/docs/kernels/README.md +21 -0
- package/docs/kernels/audio-fairlight-kernel.md +22 -0
- package/docs/kernels/color-grade-kernel.md +51 -0
- package/docs/kernels/fusion-composition-kernel.md +17 -0
- package/docs/kernels/media-pool-ingest-kernel.md +20 -0
- package/docs/kernels/render-deliver-kernel.md +32 -0
- package/docs/kernels/timeline-conform-interchange-kernel.md +39 -0
- package/docs/kernels/timeline-edit-kernel.md +17 -0
- package/docs/process/release-process.md +29 -7
- package/docs/reference/api-coverage.md +1 -1
- package/docs/reference/api-limitations.md +38 -1
- package/install.py +27 -4
- package/package.json +23 -2
- package/resolve-advanced/README.md +194 -0
- package/resolve-advanced/package.json +50 -0
- package/resolve-advanced/server/black-balance.mjs +92 -0
- package/resolve-advanced/server/capabilities.mjs +86 -0
- package/resolve-advanced/server/cdl-io.mjs +76 -0
- package/resolve-advanced/server/contrast-normalize.mjs +180 -0
- package/resolve-advanced/server/db-patch.mjs +91 -0
- package/resolve-advanced/server/deliverable-entities.mjs +86 -0
- package/resolve-advanced/server/deliverable-qc.mjs +270 -0
- package/resolve-advanced/server/editorial.mjs +370 -0
- package/resolve-advanced/server/exposure-level.mjs +129 -0
- package/resolve-advanced/server/extract-frames.mjs +196 -0
- package/resolve-advanced/server/ffprobe-media.mjs +88 -0
- package/resolve-advanced/server/gamut-legal.mjs +112 -0
- package/resolve-advanced/server/grade-body-patch.mjs +246 -0
- package/resolve-advanced/server/grade-transfer.mjs +66 -0
- package/resolve-advanced/server/group-grade-read.mjs +137 -0
- package/resolve-advanced/server/index.mjs +105 -0
- package/resolve-advanced/server/lib.mjs +84 -0
- package/resolve-advanced/server/libs.mjs +65 -0
- package/resolve-advanced/server/lineage-db.mjs +479 -0
- package/resolve-advanced/server/lut-apply.mjs +61 -0
- package/resolve-advanced/server/match-to-reference.mjs +173 -0
- package/resolve-advanced/server/media-inventory.mjs +169 -0
- package/resolve-advanced/server/media-ops.mjs +243 -0
- package/resolve-advanced/server/node-meta-db.mjs +62 -0
- package/resolve-advanced/server/node-provenance.mjs +102 -0
- package/resolve-advanced/server/offline-ref-db.mjs +420 -0
- package/resolve-advanced/server/offline-ref.mjs +179 -0
- package/resolve-advanced/server/project-db.mjs +233 -0
- package/resolve-advanced/server/provenance-audit.mjs +210 -0
- package/resolve-advanced/server/qc-frame.mjs +224 -0
- package/resolve-advanced/server/qc-sampler.mjs +67 -0
- package/resolve-advanced/server/readback.mjs +77 -0
- package/resolve-advanced/server/render-manifest.mjs +93 -0
- package/resolve-advanced/server/reverse-clip-db.mjs +270 -0
- package/resolve-advanced/server/runner-apply-contract.mjs +64 -0
- package/resolve-advanced/server/runner.mjs +244 -0
- package/resolve-advanced/server/saturation-match.mjs +108 -0
- package/resolve-advanced/server/scope-read.mjs +257 -0
- package/resolve-advanced/server/season-look.mjs +96 -0
- package/resolve-advanced/server/shot-intent.mjs +82 -0
- package/resolve-advanced/server/shot-match.mjs +147 -0
- package/resolve-advanced/server/skin-match.mjs +205 -0
- package/resolve-advanced/server/spec-compile.mjs +217 -0
- package/resolve-advanced/server/tone-curve-transfer.mjs +184 -0
- package/resolve-advanced/server/tool-catalog.mjs +265 -0
- package/resolve-advanced/server/tools/audio.mjs +54 -0
- package/resolve-advanced/server/tools/audio_plan.mjs +58 -0
- package/resolve-advanced/server/tools/capabilities.mjs +16 -0
- package/resolve-advanced/server/tools/color_trace.mjs +165 -0
- package/resolve-advanced/server/tools/conform.mjs +291 -0
- package/resolve-advanced/server/tools/deliverable.mjs +105 -0
- package/resolve-advanced/server/tools/drp.mjs +232 -0
- package/resolve-advanced/server/tools/drt.mjs +200 -0
- package/resolve-advanced/server/tools/drx.mjs +943 -0
- package/resolve-advanced/server/tools/editorial.mjs +69 -0
- package/resolve-advanced/server/tools/fairlight.mjs +108 -0
- package/resolve-advanced/server/tools/fusion.mjs +64 -0
- package/resolve-advanced/server/tools/media.mjs +102 -0
- package/resolve-advanced/server/tools/offline_ref.mjs +123 -0
- package/resolve-advanced/server/tools/pipeline.mjs +137 -0
- package/resolve-advanced/server/tools/project_db.mjs +200 -0
- package/resolve-advanced/server/tools/project_read.mjs +212 -0
- package/resolve-advanced/server/tools/provenance.mjs +73 -0
- package/resolve-advanced/server/verify-grade.mjs +113 -0
- package/resolve-advanced/server/white-balance-match.mjs +122 -0
- package/resolve-advanced/vendor/audio/format-converter.js +330 -0
- package/resolve-advanced/vendor/audio/split.js +173 -0
- package/resolve-advanced/vendor/audio/trim.js +188 -0
- package/resolve-advanced/vendor/audio-fairlight/index.js +391 -0
- package/resolve-advanced/vendor/conform-qc/adapters/frame-sampler.js +56 -0
- package/resolve-advanced/vendor/conform-qc/adapters/local-ffmpeg-sampler.js +65 -0
- package/resolve-advanced/vendor/conform-qc/adapters/rendernode-ffmpeg-sampler.js +50 -0
- package/resolve-advanced/vendor/conform-qc/adapters/resolve/driver.py +176 -0
- package/resolve-advanced/vendor/conform-qc/adapters/resolve-driver.js +61 -0
- package/resolve-advanced/vendor/conform-qc/adapters/resolve-headless-driver.js +86 -0
- package/resolve-advanced/vendor/conform-qc/adapters/vision-validator.js +75 -0
- package/resolve-advanced/vendor/conform-qc/cli.js +63 -0
- package/resolve-advanced/vendor/conform-qc/compare/advisory.js +106 -0
- package/resolve-advanced/vendor/conform-qc/compare/decode.js +36 -0
- package/resolve-advanced/vendor/conform-qc/compare/index.js +61 -0
- package/resolve-advanced/vendor/conform-qc/compare/locate.js +70 -0
- package/resolve-advanced/vendor/conform-qc/compare/metrics.js +409 -0
- package/resolve-advanced/vendor/conform-qc/docs/runbook.md +72 -0
- package/resolve-advanced/vendor/conform-qc/index.js +49 -0
- package/resolve-advanced/vendor/conform-qc/knowledge/index.js +110 -0
- package/resolve-advanced/vendor/conform-qc/ops/conform-core.js +137 -0
- package/resolve-advanced/vendor/conform-qc/ops/conform-diff.js +109 -0
- package/resolve-advanced/vendor/conform-qc/ops/insert.js +108 -0
- package/resolve-advanced/vendor/conform-qc/ops/local-conform.js +61 -0
- package/resolve-advanced/vendor/conform-qc/ops/patch.js +80 -0
- package/resolve-advanced/vendor/conform-qc/ops/trigger.js +32 -0
- package/resolve-advanced/vendor/conform-qc/ops/verify.js +105 -0
- package/resolve-advanced/vendor/conform-qc/ops/workflow.js +30 -0
- package/resolve-advanced/vendor/conform-qc/oracle/emulate.js +155 -0
- package/resolve-advanced/vendor/conform-qc/oracle/index.js +38 -0
- package/resolve-advanced/vendor/conform-qc/oracle/resolve.js +196 -0
- package/resolve-advanced/vendor/conform-qc/package.json +16 -0
- package/resolve-advanced/vendor/conform-qc/packaging/emit-aaf.js +30 -0
- package/resolve-advanced/vendor/conform-qc/packaging/emit-fcp7.js +64 -0
- package/resolve-advanced/vendor/conform-qc/packaging/index.js +169 -0
- package/resolve-advanced/vendor/conform-qc/packaging/media-ops.js +56 -0
- package/resolve-advanced/vendor/conform-qc/packaging/otio.js +50 -0
- package/resolve-advanced/vendor/conform-qc/packaging/relink.js +50 -0
- package/resolve-advanced/vendor/conform-qc/packaging/surgical-relink.js +195 -0
- package/resolve-advanced/vendor/conform-qc/parse/index.js +54 -0
- package/resolve-advanced/vendor/conform-qc/parse/merge-dto.js +27 -0
- package/resolve-advanced/vendor/conform-qc/parse/xmeml-geometry.js +256 -0
- package/resolve-advanced/vendor/conform-qc/reference/burnin-ocr.js +100 -0
- package/resolve-advanced/vendor/conform-qc/reference/sidecar.js +37 -0
- package/resolve-advanced/vendor/conform-qc/repair/analysis.js +62 -0
- package/resolve-advanced/vendor/conform-qc/repair/index.js +78 -0
- package/resolve-advanced/vendor/conform-qc/repair/media-analysis.js +139 -0
- package/resolve-advanced/vendor/conform-qc/repair/media-index.js +135 -0
- package/resolve-advanced/vendor/conform-qc/repair/strategies.js +250 -0
- package/resolve-advanced/vendor/conform-qc/report/index.js +133 -0
- package/resolve-advanced/vendor/conform-qc/roles/index.js +96 -0
- package/resolve-advanced/vendor/conform-qc/synthetic/generate.js +233 -0
- package/resolve-advanced/vendor/conform-qc/test/advisory.test.js +85 -0
- package/resolve-advanced/vendor/conform-qc/test/cli.test.js +39 -0
- package/resolve-advanced/vendor/conform-qc/test/compare.test.js +152 -0
- package/resolve-advanced/vendor/conform-qc/test/conform-diff.test.js +70 -0
- package/resolve-advanced/vendor/conform-qc/test/emulate.test.js +51 -0
- package/resolve-advanced/vendor/conform-qc/test/frame-sampler.test.js +30 -0
- package/resolve-advanced/vendor/conform-qc/test/geometry.test.js +112 -0
- package/resolve-advanced/vendor/conform-qc/test/knowledge.test.js +56 -0
- package/resolve-advanced/vendor/conform-qc/test/lifecycle.test.js +110 -0
- package/resolve-advanced/vendor/conform-qc/test/local-ffmpeg-sampler.test.js +66 -0
- package/resolve-advanced/vendor/conform-qc/test/locate.test.js +63 -0
- package/resolve-advanced/vendor/conform-qc/test/media-analysis.test.js +63 -0
- package/resolve-advanced/vendor/conform-qc/test/media-index-normalized.test.js +65 -0
- package/resolve-advanced/vendor/conform-qc/test/media-index-origin.test.js +53 -0
- package/resolve-advanced/vendor/conform-qc/test/metrics-fast.test.js +91 -0
- package/resolve-advanced/vendor/conform-qc/test/oracle-reverse.test.js +86 -0
- package/resolve-advanced/vendor/conform-qc/test/oracle.test.js +136 -0
- package/resolve-advanced/vendor/conform-qc/test/p0-acceptance.test.js +60 -0
- package/resolve-advanced/vendor/conform-qc/test/p1-acceptance.test.js +75 -0
- package/resolve-advanced/vendor/conform-qc/test/p45-patch-acceptance.test.js +102 -0
- package/resolve-advanced/vendor/conform-qc/test/packaging.test.js +133 -0
- package/resolve-advanced/vendor/conform-qc/test/parse-stubs.test.js +28 -0
- package/resolve-advanced/vendor/conform-qc/test/repair-strategies.test.js +128 -0
- package/resolve-advanced/vendor/conform-qc/test/repair.test.js +84 -0
- package/resolve-advanced/vendor/conform-qc/test/report.test.js +52 -0
- package/resolve-advanced/vendor/conform-qc/test/resolve-readback.test.js +71 -0
- package/resolve-advanced/vendor/conform-qc/test/roles.test.js +107 -0
- package/resolve-advanced/vendor/conform-qc/test/runbook.test.js +30 -0
- package/resolve-advanced/vendor/conform-qc/test/scale-residual.test.js +40 -0
- package/resolve-advanced/vendor/conform-qc/test/sidecar.test.js +51 -0
- package/resolve-advanced/vendor/conform-qc/test/smoke.test.js +87 -0
- package/resolve-advanced/vendor/conform-qc/test/surgical-relink.test.js +73 -0
- package/resolve-advanced/vendor/conform-qc/test/synthetic.test.js +98 -0
- package/resolve-advanced/vendor/conform-qc/test/verify.test.js +93 -0
- package/resolve-advanced/vendor/conform-qc/test/vision-validator.test.js +43 -0
- package/resolve-advanced/vendor/conform-qc/test/workflow-driver.test.js +46 -0
- package/resolve-advanced/vendor/conform-qc/toolset/index.js +53 -0
- package/resolve-advanced/vendor/drp-format/README.md +116 -0
- package/resolve-advanced/vendor/drp-format/__tests__/_resolve-verify.js +39 -0
- package/resolve-advanced/vendor/drp-format/__tests__/add-media-clip.test.js +30 -0
- package/resolve-advanced/vendor/drp-format/__tests__/assemble-timeline.test.js +68 -0
- package/resolve-advanced/vendor/drp-format/__tests__/author-project.test.js +57 -0
- package/resolve-advanced/vendor/drp-format/__tests__/composition-text.test.js +98 -0
- package/resolve-advanced/vendor/drp-format/__tests__/diff.test.js +272 -0
- package/resolve-advanced/vendor/drp-format/__tests__/effect-filters-compressed.test.js +143 -0
- package/resolve-advanced/vendor/drp-format/__tests__/fixtures/p0-3-session30-empty-effectfilters.drp +0 -0
- package/resolve-advanced/vendor/drp-format/__tests__/fixtures/retimed-timemap-50pct.hex +1 -0
- package/resolve-advanced/vendor/drp-format/__tests__/fixtures/retimed-timemap-dynamic.hex +1 -0
- package/resolve-advanced/vendor/drp-format/__tests__/inject-grades.test.js +238 -0
- package/resolve-advanced/vendor/drp-format/__tests__/keyed-dict.test.js +85 -0
- package/resolve-advanced/vendor/drp-format/__tests__/lut-refs.test.js +119 -0
- package/resolve-advanced/vendor/drp-format/__tests__/media-blobs.test.js +25 -0
- package/resolve-advanced/vendor/drp-format/__tests__/media-timemap.test.js +119 -0
- package/resolve-advanced/vendor/drp-format/__tests__/place-fusion-title.test.js +52 -0
- package/resolve-advanced/vendor/drp-format/__tests__/place-generator.test.js +40 -0
- package/resolve-advanced/vendor/drp-format/__tests__/place-transition.test.js +48 -0
- package/resolve-advanced/vendor/drp-format/__tests__/protobuf-wire.test.js +64 -0
- package/resolve-advanced/vendor/drp-format/__tests__/relink-media.test.js +106 -0
- package/resolve-advanced/vendor/drp-format/__tests__/splice-clips.test.js +256 -0
- package/resolve-advanced/vendor/drp-format/assemble-timeline.js +65 -0
- package/resolve-advanced/vendor/drp-format/author-project.js +118 -0
- package/resolve-advanced/vendor/drp-format/composition-text.js +213 -0
- package/resolve-advanced/vendor/drp-format/curves-encoder.js +763 -0
- package/resolve-advanced/vendor/drp-format/diff.js +310 -0
- package/resolve-advanced/vendor/drp-format/drp-packager.js +201 -0
- package/resolve-advanced/vendor/drp-format/drp-validator.js +809 -0
- package/resolve-advanced/vendor/drp-format/effect-encoder.js +640 -0
- package/resolve-advanced/vendor/drp-format/extract-lut-refs.js +119 -0
- package/resolve-advanced/vendor/drp-format/grade-encoder.example.js +133 -0
- package/resolve-advanced/vendor/drp-format/grade-encoder.js +401 -0
- package/resolve-advanced/vendor/drp-format/grade-encoder.test.js +171 -0
- package/resolve-advanced/vendor/drp-format/grade-encoder.verify.js +249 -0
- package/resolve-advanced/vendor/drp-format/grade-node-extractor.js +495 -0
- package/resolve-advanced/vendor/drp-format/grade-parameter-decoder.js +774 -0
- package/resolve-advanced/vendor/drp-format/index.js +174 -0
- package/resolve-advanced/vendor/drp-format/inject-grades.js +219 -0
- package/resolve-advanced/vendor/drp-format/keyed-dict.js +227 -0
- package/resolve-advanced/vendor/drp-format/marker-encoder.js +532 -0
- package/resolve-advanced/vendor/drp-format/media-blobs.js +53 -0
- package/resolve-advanced/vendor/drp-format/media-timemap.js +164 -0
- package/resolve-advanced/vendor/drp-format/mp-folder-builder.js +293 -0
- package/resolve-advanced/vendor/drp-format/node-tree-encoder.js +1246 -0
- package/resolve-advanced/vendor/drp-format/package.json +18 -0
- package/resolve-advanced/vendor/drp-format/place-fusion-title.js +131 -0
- package/resolve-advanced/vendor/drp-format/place-generator.js +77 -0
- package/resolve-advanced/vendor/drp-format/place-transition.js +91 -0
- package/resolve-advanced/vendor/drp-format/power-window-encoder.js +776 -0
- package/resolve-advanced/vendor/drp-format/protobuf-wire.js +111 -0
- package/resolve-advanced/vendor/drp-format/qualifier-encoder.js +706 -0
- package/resolve-advanced/vendor/drp-format/relink-media.js +218 -0
- package/resolve-advanced/vendor/drp-format/rich-title-encoder.js +447 -0
- package/resolve-advanced/vendor/drp-format/seq-container-builder.js +572 -0
- package/resolve-advanced/vendor/drp-format/seq-surgery.js +155 -0
- package/resolve-advanced/vendor/drp-format/splice-clips.js +445 -0
- package/resolve-advanced/vendor/drp-format/templates/empty-project.drp +0 -0
- package/resolve-advanced/vendor/drp-format/templates/fusion-title.xml +56 -0
- package/resolve-advanced/vendor/drp-format/templates/generator-solid-color.xml +21 -0
- package/resolve-advanced/vendor/drp-format/templates/media-clip-h264.drp +0 -0
- package/resolve-advanced/vendor/drp-format/templates/transition-cross-dissolve.xml +22 -0
- package/resolve-advanced/vendor/drp-format/tool-registry.descriptor.js +80 -0
- package/resolve-advanced/vendor/drp-format/utils/safe-archive.js +54 -0
- package/resolve-advanced/vendor/drp-format/xml-builder.js +759 -0
- package/resolve-advanced/vendor/drt-format/__tests__/_resolve-verify.js +21 -0
- package/resolve-advanced/vendor/drt-format/__tests__/build.test.js +113 -0
- package/resolve-advanced/vendor/drt-format/__tests__/multi-format.test.js +93 -0
- package/resolve-advanced/vendor/drt-format/__tests__/parse-real-resolve21.test.js +51 -0
- package/resolve-advanced/vendor/drt-format/__tests__/parse.test.js +134 -0
- package/resolve-advanced/vendor/drt-format/__tests__/resolve-versions.test.js +120 -0
- package/resolve-advanced/vendor/drt-format/__tests__/schema-fingerprint.test.js +62 -0
- package/resolve-advanced/vendor/drt-format/__tests__/smoke.test.js +27 -0
- package/resolve-advanced/vendor/drt-format/__tests__/validate.test.js +109 -0
- package/resolve-advanced/vendor/drt-format/capabilities.js +69 -0
- package/resolve-advanced/vendor/drt-format/capability-domains.js +56 -0
- package/resolve-advanced/vendor/drt-format/drt-builder.js +42 -0
- package/resolve-advanced/vendor/drt-format/drt-parser.js +164 -0
- package/resolve-advanced/vendor/drt-format/drt-validator.js +98 -0
- package/resolve-advanced/vendor/drt-format/index.js +45 -0
- package/resolve-advanced/vendor/drt-format/package.json +17 -0
- package/resolve-advanced/vendor/drt-format/resolve-versions.js +153 -0
- package/resolve-advanced/vendor/drt-format/schema-fingerprint.js +74 -0
- package/resolve-advanced/vendor/drt-format/templates/drt-project-shell.xml +124 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/curves-roundtrip.test.js +148 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-1-face-refinement.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-1-filmgrain.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-1-glow.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-1-lens-flare.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-3-beauty-25.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p2-3-beauty-75.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p5-1-node-no-lut.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/fixtures/p5-1-node-with-lut.drx +58 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/hsl-curves-roundtrip.test.js +117 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/lut-refs-roundtrip.test.js +112 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/matte-roundtrip.test.js +167 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/ofx-roundtrip.test.js +215 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/ofx-slugs-discovery.test.js +149 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/power-window-roundtrip.test.js +230 -0
- package/resolve-advanced/vendor/drx-codec/__tests__/qualifier-roundtrip.test.js +226 -0
- package/resolve-advanced/vendor/drx-codec/cdl-exporter.js +470 -0
- package/resolve-advanced/vendor/drx-codec/color-nlp-parser.js +833 -0
- package/resolve-advanced/vendor/drx-codec/data/color-nlp-action-mappings.json +2232 -0
- package/resolve-advanced/vendor/drx-codec/drx-generator.js +4123 -0
- package/resolve-advanced/vendor/drx-codec/drx-merger.js +320 -0
- package/resolve-advanced/vendor/drx-codec/drx-parser.js +1808 -0
- package/resolve-advanced/vendor/drx-codec/extract-custom-curves.js +215 -0
- package/resolve-advanced/vendor/drx-codec/extract-hsl-curves.js +180 -0
- package/resolve-advanced/vendor/drx-codec/extract-lut-refs.js +158 -0
- package/resolve-advanced/vendor/drx-codec/extract-matte-finesse.js +83 -0
- package/resolve-advanced/vendor/drx-codec/extract-ofx-params.js +226 -0
- package/resolve-advanced/vendor/drx-codec/extract-power-window.js +184 -0
- package/resolve-advanced/vendor/drx-codec/extract-qualifier.js +99 -0
- package/resolve-advanced/vendor/drx-codec/index.js +109 -0
- package/resolve-advanced/vendor/drx-codec/node-layout.js +249 -0
- package/resolve-advanced/vendor/drx-codec/resolved-drx-generator.js +393 -0
- package/resolve-advanced/vendor/drx-codec/tool-resolver.js +974 -0
- package/resolve-advanced/vendor/drx-codec/training-examples.js +548 -0
- package/resolve-advanced/vendor/drx-codec/vocabulary-intents.js +539 -0
- package/resolve-advanced/vendor/drx-parameters/CALIBRATION-STATUS.md +319 -0
- package/resolve-advanced/vendor/drx-parameters/DRX-VALUE-SCALING.md +251 -0
- package/resolve-advanced/vendor/drx-parameters/calibrated-ranges.json +17737 -0
- package/resolve-advanced/vendor/drx-parameters/corrector-types.js +120 -0
- package/resolve-advanced/vendor/drx-parameters/index.js +193 -0
- package/resolve-advanced/vendor/drx-parameters/parameter-codec.js +451 -0
- package/resolve-advanced/vendor/drx-parameters/parameter-ids.js +1292 -0
- package/resolve-advanced/vendor/drx-parameters/parameter-ranges.js +570 -0
- package/resolve-advanced/vendor/drx-parameters/parameter-validator.js +392 -0
- package/resolve-advanced/vendor/drx-parameters/resolvefx-registry.json +14973 -0
- package/resolve-advanced/vendor/drx-parameters/tool-registry.descriptor.js +38 -0
- package/resolve-advanced/vendor/drx-parameters/tools/drx-analyzer.js +371 -0
- package/resolve-advanced/vendor/fairlight/index.js +833 -0
- package/resolve-advanced/vendor/fusion-codec/composition-generator.js +401 -0
- package/resolve-advanced/vendor/fusion-codec/templates/blur-region.js +103 -0
- package/resolve-advanced/vendor/fusion-codec/templates/color-correct.js +87 -0
- package/resolve-advanced/vendor/fusion-codec/templates/film-grain.js +106 -0
- package/resolve-advanced/vendor/fusion-codec/templates/lower-third.js +241 -0
- package/resolve-advanced/vendor/fusion-codec/templates/picture-in-picture.js +188 -0
- package/resolve-advanced/vendor/fusion-codec/templates/text-overlay.js +161 -0
- package/resolve-advanced/vendor/fusion-codec/templates/title-card.js +184 -0
- package/resolve-advanced/vendor/fusion-codec/templates/vignette.js +107 -0
- package/resolve-advanced/vendor/fusion-codec/templates/watermark.js +101 -0
- package/scripts/agent-rules/README.md +68 -0
- package/src/analysis_dashboard.py +233 -1
- package/src/granular/common.py +1 -1
- package/src/server.py +479 -29
- package/src/utils/api_truth.py +117 -0
- package/src/utils/cut_ir.py +1 -1
- package/src/utils/failure_tracker.py +0 -2
- package/src/utils/timeline_xml.py +405 -0
|
@@ -0,0 +1,4123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DRX Generator - Programmatic DaVinci Resolve Grade Creation
|
|
3
|
+
*
|
|
4
|
+
* Generates DRX (DaVinci Resolve eXchange) files from color parameters.
|
|
5
|
+
* Based on reverse-engineered format: [0x81] + [ZSTD-compressed protobuf]
|
|
6
|
+
*
|
|
7
|
+
* Uses shared DRX parameter library for consistent parameter handling
|
|
8
|
+
* across the The project platform.
|
|
9
|
+
*
|
|
10
|
+
* @module drx/drx-generator
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// ZSTD compression backend selection — priority:
|
|
14
|
+
// 1. Node's built-in zlib.zstdCompressSync (Node 22+, available on Vercel)
|
|
15
|
+
// — reliable, no buffer truncation bugs
|
|
16
|
+
// 2. zstd-codec (WASM) — falls back here for older Node (Electron dev)
|
|
17
|
+
// 3. fzstd (pure JS) — decompress only; compress is undefined, so this
|
|
18
|
+
// fallback exists only for completeness and will throw if reached
|
|
19
|
+
//
|
|
20
|
+
// HISTORY: zstd-codec's Simple.compress() was observed truncating frames on
|
|
21
|
+
// Vercel's Node 22 runtime (body would be valid zstd magic but the compressed
|
|
22
|
+
// stream would end prematurely, producing a DRX Resolve couldn't parse and
|
|
23
|
+
// silently applied as an empty node graph — the clip grade would not change).
|
|
24
|
+
// Using node:zlib.zstdCompressSync when available eliminates that class of
|
|
25
|
+
// bug entirely.
|
|
26
|
+
// Debug tracing is stdout-hostile inside a stdio MCP server — gate it behind DRX_DEBUG.
|
|
27
|
+
const debugLog = process.env.DRX_DEBUG ? console.log.bind(console) : () => {};
|
|
28
|
+
const nodeZlib = require('zlib');
|
|
29
|
+
const HAS_NATIVE_ZSTD = typeof nodeZlib.zstdCompressSync === 'function';
|
|
30
|
+
|
|
31
|
+
let ZstdCodec;
|
|
32
|
+
try {
|
|
33
|
+
ZstdCodec = require('zstd-codec').ZstdCodec;
|
|
34
|
+
} catch {
|
|
35
|
+
ZstdCodec = null;
|
|
36
|
+
}
|
|
37
|
+
const { v4: uuidv4 } = require('uuid');
|
|
38
|
+
|
|
39
|
+
// Import shared DRX parameter library
|
|
40
|
+
const drxParams = require('../drx-parameters');
|
|
41
|
+
|
|
42
|
+
// ZSTD codec instance (initialized lazily)
|
|
43
|
+
let zstdCompressor = null;
|
|
44
|
+
|
|
45
|
+
async function getZstdCompressor() {
|
|
46
|
+
if (zstdCompressor) return zstdCompressor;
|
|
47
|
+
|
|
48
|
+
// Native Node zstd (Node 22+). Preferred path on Vercel.
|
|
49
|
+
if (HAS_NATIVE_ZSTD) {
|
|
50
|
+
zstdCompressor = {
|
|
51
|
+
compress(data) {
|
|
52
|
+
const buf = data instanceof Buffer
|
|
53
|
+
? data
|
|
54
|
+
: Buffer.from(data instanceof Uint8Array ? data : new Uint8Array(data));
|
|
55
|
+
return nodeZlib.zstdCompressSync(buf);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
return zstdCompressor;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// zstd-codec (WASM) — Electron/older-Node fallback
|
|
62
|
+
if (ZstdCodec) {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
ZstdCodec.run((zstd) => {
|
|
65
|
+
zstdCompressor = new zstd.Simple();
|
|
66
|
+
resolve(zstdCompressor);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Final fallback — fzstd only exposes decompress, so this path will
|
|
72
|
+
// throw at call time. Kept so the error surfaces clearly rather than
|
|
73
|
+
// silently returning undefined and producing a malformed DRX.
|
|
74
|
+
const fzstd = require('fzstd');
|
|
75
|
+
zstdCompressor = {
|
|
76
|
+
compress(data) {
|
|
77
|
+
if (typeof fzstd.compress !== 'function') {
|
|
78
|
+
throw new Error('No zstd compressor available: node:zlib zstd requires Node 22+, zstd-codec failed to load, and fzstd does not support compression.');
|
|
79
|
+
}
|
|
80
|
+
return Buffer.from(fzstd.compress(data instanceof Uint8Array ? data : new Uint8Array(data)));
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
return zstdCompressor;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Parameter ID mappings from shared library
|
|
88
|
+
* See: /lib/drx-parameters for full documentation
|
|
89
|
+
*/
|
|
90
|
+
const PARAM_IDS = {
|
|
91
|
+
// Primary Corrector - Lift
|
|
92
|
+
LIFT_R: drxParams.LIFT.R,
|
|
93
|
+
LIFT_G: drxParams.LIFT.G,
|
|
94
|
+
LIFT_B: drxParams.LIFT.B,
|
|
95
|
+
LIFT_MASTER: drxParams.LIFT.MASTER,
|
|
96
|
+
|
|
97
|
+
// Primary Corrector - Gain
|
|
98
|
+
GAIN_R: drxParams.GAIN.R,
|
|
99
|
+
GAIN_G: drxParams.GAIN.G,
|
|
100
|
+
GAIN_B: drxParams.GAIN.B,
|
|
101
|
+
GAIN_MASTER: drxParams.GAIN.MASTER,
|
|
102
|
+
|
|
103
|
+
// Primary Corrector - Gamma
|
|
104
|
+
GAMMA_R: drxParams.GAMMA.R,
|
|
105
|
+
GAMMA_G: drxParams.GAMMA.G,
|
|
106
|
+
GAMMA_B: drxParams.GAMMA.B,
|
|
107
|
+
GAMMA_MASTER: drxParams.GAMMA.MASTER,
|
|
108
|
+
|
|
109
|
+
// Primary Corrector - Offset
|
|
110
|
+
OFFSET_R: drxParams.OFFSET.R,
|
|
111
|
+
OFFSET_G: drxParams.OFFSET.G,
|
|
112
|
+
OFFSET_B: drxParams.OFFSET.B,
|
|
113
|
+
|
|
114
|
+
// Saturation
|
|
115
|
+
SATURATION_PRIMARY: drxParams.SATURATION.PRIMARY,
|
|
116
|
+
// Hue Rotate — uses SATURATION.LEGACY ID (0x06000004) with nested {F1: float} encoding
|
|
117
|
+
// Encoding: DRX = (UI - 50) / 50. UI range 0-100, default 50.
|
|
118
|
+
// Confirmed 2026-03-18: UI 92 → DRX 0.84, UI 75 → DRX 0.50
|
|
119
|
+
HUE_ROTATE: drxParams.SATURATION.LEGACY, // 0x06000004
|
|
120
|
+
|
|
121
|
+
// Contrast / Pivot (shared controls range — corrected 2026-03-22)
|
|
122
|
+
// NOTE: The old CONTRAST block (0x0830xxxx) was a misidentification of HSL Qualifier params.
|
|
123
|
+
// Real contrast/pivot are in the 0x860000C0 range, already mapped as LOG_CONTRAST and PIVOT_PRIMARY below.
|
|
124
|
+
|
|
125
|
+
// Temperature/Tint (all in Primary corrector, Type 1)
|
|
126
|
+
TEMPERATURE: drxParams.TEMP_TINT.TEMPERATURE,
|
|
127
|
+
TINT: drxParams.TEMP_TINT.TINT,
|
|
128
|
+
COLOR_BOOST: drxParams.TEMP_TINT.COLOR_BOOST,
|
|
129
|
+
MIDTONE_DETAIL: drxParams.TEMP_TINT.MIDTONE_DETAIL,
|
|
130
|
+
LOG_CONTRAST: drxParams.TEMP_TINT.CONTRAST,
|
|
131
|
+
// Pivot — TEMP_TINT.CONTRAST - 1, confirmed by manual DRX capture 2026-03-17
|
|
132
|
+
PIVOT_PRIMARY: 2248147136,
|
|
133
|
+
|
|
134
|
+
// Soft Clip — per-channel R/G/B in Primary corrector
|
|
135
|
+
// Confirmed by manual DRX capture 2026-03-17: SoftClipHigh=70 → IDs 100663350-352 at 0.70
|
|
136
|
+
SOFT_CLIP_HIGH_R: 100663350,
|
|
137
|
+
SOFT_CLIP_HIGH_G: 100663351,
|
|
138
|
+
SOFT_CLIP_HIGH_B: 100663352,
|
|
139
|
+
// Soft Clip Low — CORRECTED 2026-03-24 via manual DRX capture autoresearch.
|
|
140
|
+
// Original extrapolation had B at 100663349 (0x35) — WRONG. Actual B is at 100663346 (0x32).
|
|
141
|
+
// Layout: B(0x32), R(0x33), G(0x34) — B is BEFORE R, not after G.
|
|
142
|
+
SOFT_CLIP_LOW_R: 100663347, // 0x06000033
|
|
143
|
+
SOFT_CLIP_LOW_G: 100663348, // 0x06000034
|
|
144
|
+
SOFT_CLIP_LOW_B: 100663346, // 0x06000032 (was incorrectly 100663349)
|
|
145
|
+
|
|
146
|
+
// Soft Clip Soft (softness controls) — captured 2026-03-18
|
|
147
|
+
// Encoding: UI / 50 (so UI 25 → DRX 0.5, UI 50 → DRX 1.0)
|
|
148
|
+
SOFT_CLIP_LOW_SOFT_R: 100663354, // 0x0600003A
|
|
149
|
+
SOFT_CLIP_LOW_SOFT_G: 100663355, // 0x0600003B
|
|
150
|
+
SOFT_CLIP_LOW_SOFT_B: 100663356, // 0x0600003C
|
|
151
|
+
SOFT_CLIP_HIGH_SOFT_R: 100663358, // 0x0600003E
|
|
152
|
+
SOFT_CLIP_HIGH_SOFT_G: 100663359, // 0x0600003F
|
|
153
|
+
SOFT_CLIP_HIGH_SOFT_B: 100663360, // 0x06000040
|
|
154
|
+
|
|
155
|
+
// Contrast Range — confirmed by manual DRX captures 2026-03-18
|
|
156
|
+
// Low Range: 0x860000CB, direct encoding (UI value = DRX value)
|
|
157
|
+
// High Range: 0x860000CC, INVERTED encoding (DRX = 1.0 - UI)
|
|
158
|
+
CONTRAST_LOW_RANGE_PRIMARY: 2248147147, // 0x860000CB
|
|
159
|
+
CONTRAST_HIGH_RANGE_PRIMARY: 2248147148, // 0x860000CC
|
|
160
|
+
|
|
161
|
+
// HDR Global
|
|
162
|
+
HDR_BLACK_OFFSET: drxParams.HDR_ZONE.BLACK_OFFSET,
|
|
163
|
+
|
|
164
|
+
// HDR Zone
|
|
165
|
+
HDR_ZONE_ADJUSTMENTS: drxParams.HDR_ZONE.ZONE_ADJUSTMENTS,
|
|
166
|
+
HDR_ZONE_DEFINITIONS: drxParams.HDR_ZONE.ZONE_DEFINITIONS,
|
|
167
|
+
HDR_ZONE_METADATA: drxParams.HDR_ZONE.ZONE_METADATA,
|
|
168
|
+
HDR_ZONE_PARAM: drxParams.HDR_ZONE.PARAM_1,
|
|
169
|
+
|
|
170
|
+
// Log Wheels (R/G/B only - no master channel)
|
|
171
|
+
// Corrected 2026-01-14: Shadow=0xc2-c4, Midtone=0xc5-c7, Highlight=0xc8-ca
|
|
172
|
+
LOG_SHADOW_R: drxParams.LOG_WHEELS.SHADOW_R,
|
|
173
|
+
LOG_SHADOW_G: drxParams.LOG_WHEELS.SHADOW_G,
|
|
174
|
+
LOG_SHADOW_B: drxParams.LOG_WHEELS.SHADOW_B,
|
|
175
|
+
LOG_MIDTONE_R: drxParams.LOG_WHEELS.MIDTONE_R,
|
|
176
|
+
LOG_MIDTONE_G: drxParams.LOG_WHEELS.MIDTONE_G,
|
|
177
|
+
LOG_MIDTONE_B: drxParams.LOG_WHEELS.MIDTONE_B,
|
|
178
|
+
LOG_HIGHLIGHT_R: drxParams.LOG_WHEELS.HIGHLIGHT_R,
|
|
179
|
+
LOG_HIGHLIGHT_G: drxParams.LOG_WHEELS.HIGHLIGHT_G,
|
|
180
|
+
LOG_HIGHLIGHT_B: drxParams.LOG_WHEELS.HIGHLIGHT_B,
|
|
181
|
+
|
|
182
|
+
// RGB Mixer (trained 2026-03-17 from rgbmixer_1.1.2.drx)
|
|
183
|
+
RGB_MIXER_RR: drxParams.RGB_MIXER?.RR,
|
|
184
|
+
RGB_MIXER_GR: drxParams.RGB_MIXER?.GR,
|
|
185
|
+
RGB_MIXER_BR: drxParams.RGB_MIXER?.BR,
|
|
186
|
+
RGB_MIXER_RG: drxParams.RGB_MIXER?.RG,
|
|
187
|
+
RGB_MIXER_GG: drxParams.RGB_MIXER?.GG,
|
|
188
|
+
RGB_MIXER_BG: drxParams.RGB_MIXER?.BG,
|
|
189
|
+
RGB_MIXER_RB: drxParams.RGB_MIXER?.RB,
|
|
190
|
+
RGB_MIXER_GB: drxParams.RGB_MIXER?.GB,
|
|
191
|
+
RGB_MIXER_BB: drxParams.RGB_MIXER?.BB,
|
|
192
|
+
|
|
193
|
+
// Custom Curves YRGB (trained 2026-03-17, CORRECTED 2026-03-18)
|
|
194
|
+
// Round-trip verification proved drx-parameters mapping was shifted:
|
|
195
|
+
// 0x86000506 = R (not Y), 0x86000507 = G (not R), 0x86000508 = B (not G), 0x86000509 = Y (not B)
|
|
196
|
+
// Metadata IDs: 0xBB=Y, 0xBD=R, 0xBE=G, 0xBF=B (unchanged)
|
|
197
|
+
CURVES_Y_META: drxParams.CUSTOM_CURVES?.Y_META, // 0x860000BB
|
|
198
|
+
CURVES_R_META: drxParams.CUSTOM_CURVES?.R_META, // 0x860000BD
|
|
199
|
+
CURVES_G_META: drxParams.CUSTOM_CURVES?.G_META, // 0x860000BE
|
|
200
|
+
CURVES_B_META: drxParams.CUSTOM_CURVES?.B_META, // 0x860000BF
|
|
201
|
+
CURVES_Y_SPLINE: 2248148233, // 0x86000509 — was incorrectly mapped to B_SPLINE
|
|
202
|
+
CURVES_R_SPLINE: 2248148230, // 0x86000506 — was incorrectly mapped to Y_SPLINE
|
|
203
|
+
CURVES_G_SPLINE: 2248148231, // 0x86000507 — was incorrectly mapped to R_SPLINE
|
|
204
|
+
CURVES_B_SPLINE: 2248148232, // 0x86000508 — was incorrectly mapped to G_SPLINE
|
|
205
|
+
|
|
206
|
+
// HSL Curves (trained 2026-03-17)
|
|
207
|
+
HSL_COMMON_FLAG: drxParams.HSL_CURVES?.COMMON_FLAG,
|
|
208
|
+
HSL_HUE_VS_HUE_SPLINE: drxParams.HSL_CURVES?.HUE_VS_HUE_SPLINE,
|
|
209
|
+
HSL_HUE_VS_SAT_SPLINE: drxParams.HSL_CURVES?.HUE_VS_SAT_SPLINE,
|
|
210
|
+
HSL_HUE_VS_LUM_SPLINE: drxParams.HSL_CURVES?.HUE_VS_LUM_SPLINE,
|
|
211
|
+
HSL_LUM_VS_SAT_SPLINE: drxParams.HSL_CURVES?.LUM_VS_SAT_SPLINE,
|
|
212
|
+
HSL_SAT_VS_SAT_SPLINE: drxParams.HSL_CURVES?.SAT_VS_SAT_SPLINE,
|
|
213
|
+
HSL_SAT_VS_LUM_SPLINE: drxParams.HSL_CURVES?.SAT_VS_LUM_SPLINE,
|
|
214
|
+
|
|
215
|
+
// Additional controls (trained 2026-03-17)
|
|
216
|
+
HIGHLIGHTS: drxParams.ADDITIONAL?.HIGHLIGHTS,
|
|
217
|
+
SHADOWS: drxParams.ADDITIONAL?.SHADOWS,
|
|
218
|
+
LUM_MIX_SLIDER: drxParams.ADDITIONAL?.LUM_MIX_SLIDER, // 0x8600000B
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Corrector Type IDs from shared library
|
|
223
|
+
*/
|
|
224
|
+
const CORRECTOR_TYPES = drxParams.CORRECTOR_TYPES;
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Presence marker IDs from shared library
|
|
228
|
+
*/
|
|
229
|
+
const PRESENCE_MARKER_IDS = drxParams.PRESENCE_MARKER_IDS;
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Default neutral grade values from shared library
|
|
233
|
+
*/
|
|
234
|
+
const NEUTRAL_GRADE = {
|
|
235
|
+
// Color Wheels
|
|
236
|
+
lift: {
|
|
237
|
+
r: drxParams.getDefault('lift', 'r'),
|
|
238
|
+
g: drxParams.getDefault('lift', 'g'),
|
|
239
|
+
b: drxParams.getDefault('lift', 'b'),
|
|
240
|
+
master: drxParams.getDefault('lift', 'master'),
|
|
241
|
+
},
|
|
242
|
+
gamma: {
|
|
243
|
+
r: drxParams.getDefault('gamma', 'r'),
|
|
244
|
+
g: drxParams.getDefault('gamma', 'g'),
|
|
245
|
+
b: drxParams.getDefault('gamma', 'b'),
|
|
246
|
+
master: drxParams.getDefault('gamma', 'master'),
|
|
247
|
+
},
|
|
248
|
+
gain: {
|
|
249
|
+
r: drxParams.getDefault('gain', 'r'),
|
|
250
|
+
g: drxParams.getDefault('gain', 'g'),
|
|
251
|
+
b: drxParams.getDefault('gain', 'b'),
|
|
252
|
+
master: drxParams.getDefault('gain', 'master'),
|
|
253
|
+
},
|
|
254
|
+
offset: {
|
|
255
|
+
r: drxParams.getDefault('offset', 'r'),
|
|
256
|
+
g: drxParams.getDefault('offset', 'g'),
|
|
257
|
+
b: drxParams.getDefault('offset', 'b'),
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
// Primary adjustments
|
|
261
|
+
saturation: drxParams.getDefault('saturation', 'master'),
|
|
262
|
+
contrast: drxParams.getDefault('contrast', 'master'),
|
|
263
|
+
pivot: drxParams.getDefault('pivotFine', 'master'),
|
|
264
|
+
|
|
265
|
+
// Temperature/Tint
|
|
266
|
+
temperature: drxParams.getDefault('temperature', 'master'),
|
|
267
|
+
tint: drxParams.getDefault('tint', 'master'),
|
|
268
|
+
|
|
269
|
+
// Detail
|
|
270
|
+
midtoneDetail: drxParams.getDefault('midtoneDetail', 'master'),
|
|
271
|
+
|
|
272
|
+
// Soft clip
|
|
273
|
+
softClipHigh: drxParams.getDefault('softClipHigh', 'master'),
|
|
274
|
+
softClipLow: drxParams.getDefault('softClipLow', 'master'),
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Encode a varint (variable-length integer) for protobuf
|
|
279
|
+
*/
|
|
280
|
+
function encodeVarint(value) {
|
|
281
|
+
const bytes = [];
|
|
282
|
+
while (value > 0x7f) {
|
|
283
|
+
bytes.push((value & 0x7f) | 0x80);
|
|
284
|
+
value >>>= 7;
|
|
285
|
+
}
|
|
286
|
+
bytes.push(value & 0x7f);
|
|
287
|
+
return Buffer.from(bytes);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Encode a float32 as little-endian bytes
|
|
292
|
+
*/
|
|
293
|
+
function encodeFloat32(value) {
|
|
294
|
+
const buf = Buffer.alloc(4);
|
|
295
|
+
buf.writeFloatLE(value, 0);
|
|
296
|
+
return buf;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Create a protobuf field with varint wire type (0)
|
|
301
|
+
*/
|
|
302
|
+
function protoVarint(fieldNum, value) {
|
|
303
|
+
const tag = (fieldNum << 3) | 0;
|
|
304
|
+
return Buffer.concat([encodeVarint(tag), encodeVarint(value)]);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Create a protobuf field with fixed32 wire type (5) for floats
|
|
309
|
+
*/
|
|
310
|
+
function protoFloat32(fieldNum, value) {
|
|
311
|
+
const tag = (fieldNum << 3) | 5;
|
|
312
|
+
return Buffer.concat([encodeVarint(tag), encodeFloat32(value)]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Create a protobuf field with fixed64 wire type (1) for doubles
|
|
317
|
+
*/
|
|
318
|
+
function protoFloat64(fieldNum, value) {
|
|
319
|
+
const tag = (fieldNum << 3) | 1;
|
|
320
|
+
const buf = Buffer.alloc(8);
|
|
321
|
+
buf.writeDoubleLE(value, 0);
|
|
322
|
+
return Buffer.concat([encodeVarint(tag), buf]);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Create a protobuf field with length-delimited wire type (2)
|
|
327
|
+
*/
|
|
328
|
+
function protoBytes(fieldNum, data) {
|
|
329
|
+
const tag = (fieldNum << 3) | 2;
|
|
330
|
+
return Buffer.concat([encodeVarint(tag), encodeVarint(data.length), data]);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Create a color parameter entry
|
|
335
|
+
* Format: Field 3 { Field 1 (ID), Field 2 { Field 1 (float value) } }
|
|
336
|
+
*/
|
|
337
|
+
function createParameterEntry(paramId, value) {
|
|
338
|
+
// ── Universal range clamp ──────────────────────────────────────────────────
|
|
339
|
+
// Every float that enters the DRX body passes through here.
|
|
340
|
+
// Look up the param ID in the shared parameter library to get valid range,
|
|
341
|
+
// then hard-clamp. This protects ALL builder functions (Primary, HDR, Curves,
|
|
342
|
+
// Qualifier, Windows, ResolveFX) with zero per-builder code.
|
|
343
|
+
// Unknown param IDs pass through unchanged (future-proof).
|
|
344
|
+
const paramInfo = drxParams.getParamInfo(paramId);
|
|
345
|
+
if (paramInfo) {
|
|
346
|
+
const range = drxParams.getRange(paramInfo.control, paramInfo.channel);
|
|
347
|
+
if (range) {
|
|
348
|
+
const clamped = Math.max(range.min, Math.min(range.max, value));
|
|
349
|
+
if (clamped !== value) {
|
|
350
|
+
console.warn(`[DRX] CLAMPED ${paramInfo.control}.${paramInfo.channel}: ${value} → ${clamped} (range ${range.min}–${range.max})`);
|
|
351
|
+
}
|
|
352
|
+
value = clamped;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Inner value message: Field 1 = float
|
|
357
|
+
const valueMsg = protoFloat32(1, value);
|
|
358
|
+
|
|
359
|
+
// Outer entry: Field 1 = ID (varint), Field 2 = value message
|
|
360
|
+
const entry = Buffer.concat([
|
|
361
|
+
protoVarint(1, paramId),
|
|
362
|
+
protoBytes(2, valueMsg),
|
|
363
|
+
]);
|
|
364
|
+
|
|
365
|
+
// Wrap in Field 3
|
|
366
|
+
return protoBytes(3, entry);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Create a varint-valued parameter entry: F3 { F1 (ID), F2 { F2 (varint) } }.
|
|
371
|
+
* Live Resolve stores flag/mode params (window type 0x88500008, qualifier mode,
|
|
372
|
+
* gradient subtype 0x08F00001) in a varint envelope — NOT the float32 F2.F1
|
|
373
|
+
* envelope createParameterEntry writes. Wire-confirmed 2026-07-01 against the
|
|
374
|
+
* power-window/gradient/qualifier fixtures (all carry {F2: <varint>}).
|
|
375
|
+
* No range clamp — these are discrete flags, not ranged floats.
|
|
376
|
+
*/
|
|
377
|
+
function createVarintParameterEntry(paramId, value) {
|
|
378
|
+
const valueMsg = protoVarint(2, value);
|
|
379
|
+
const entry = Buffer.concat([
|
|
380
|
+
protoVarint(1, paramId),
|
|
381
|
+
protoBytes(2, valueMsg),
|
|
382
|
+
]);
|
|
383
|
+
return protoBytes(3, entry);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Create a curve control point
|
|
388
|
+
* Format: Field 1 { Field 1 (x float), Field 2 (y float) }
|
|
389
|
+
*/
|
|
390
|
+
function createCurvePoint(x, y) {
|
|
391
|
+
const point = Buffer.concat([
|
|
392
|
+
protoFloat32(1, x),
|
|
393
|
+
protoFloat32(2, y),
|
|
394
|
+
]);
|
|
395
|
+
return protoBytes(1, point);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Encode a YRGB curve spline as nested protobuf for DRX.
|
|
400
|
+
* Coordinate space: 0–1023. Sentinel: (-1024, -1024). End: (1023, 1023).
|
|
401
|
+
* @param {Array<{x: number, y: number}>} points - Control points in 0–1 normalized space
|
|
402
|
+
* @returns {Buffer} Encoded spline data
|
|
403
|
+
*/
|
|
404
|
+
function encodeCurveSpline(points) {
|
|
405
|
+
const SCALE = 1023;
|
|
406
|
+
const bufs = [];
|
|
407
|
+
bufs.push(createCurvePoint(-1024, -1024)); // sentinel
|
|
408
|
+
bufs.push(protoBytes(1, Buffer.alloc(0))); // empty separator
|
|
409
|
+
for (const pt of points) {
|
|
410
|
+
bufs.push(createCurvePoint(
|
|
411
|
+
Math.max(0, Math.min(SCALE, pt.x * SCALE)),
|
|
412
|
+
Math.max(0, Math.min(SCALE, pt.y * SCALE)),
|
|
413
|
+
));
|
|
414
|
+
}
|
|
415
|
+
bufs.push(createCurvePoint(SCALE, SCALE)); // end
|
|
416
|
+
return protoBytes(8, Buffer.concat(bufs));
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Encode an HSL curve spline as nested protobuf for DRX.
|
|
421
|
+
* Coordinate space: raw floats (X = 0.0–1.0 hue fraction with wrap, Y = 0.0–1.0 where 0.5 = neutral).
|
|
422
|
+
* Training samples show X ranges from ~-0.08 to ~1.08 (wrap-around continuity).
|
|
423
|
+
* @param {Array<{x: number, y: number}>} points - Control points in raw float space
|
|
424
|
+
* @returns {Buffer} Encoded spline data
|
|
425
|
+
*/
|
|
426
|
+
function encodeHSLCurveSpline(points) {
|
|
427
|
+
// HSL curves use a different convention from YRGB:
|
|
428
|
+
// - NO sentinel (-1024, -1024)
|
|
429
|
+
// - NO empty separator
|
|
430
|
+
// - NO end marker
|
|
431
|
+
// - Just raw float data points in F8 wrapper
|
|
432
|
+
// - Points must cover wrap-around range (~-0.08 to ~1.08 in X)
|
|
433
|
+
// - Y: 0.5 = neutral, >0.5 = boost, <0.5 = cut
|
|
434
|
+
// Training evidence: hue_vs_sat_1.1.2.drx has 40 raw points, no sentinels.
|
|
435
|
+
const bufs = [];
|
|
436
|
+
for (const pt of points) {
|
|
437
|
+
bufs.push(createCurvePoint(pt.x, pt.y));
|
|
438
|
+
}
|
|
439
|
+
return protoBytes(8, Buffer.concat(bufs));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Encode curve metadata (point count) as nested protobuf { F2: varint count }
|
|
444
|
+
*/
|
|
445
|
+
function encodeCurveMeta(count) {
|
|
446
|
+
return protoVarint(2, count);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Build Custom Curves (YRGB) parameters.
|
|
451
|
+
* Input: colorParams.customCurves = { y: [{x,y}], r: [{x,y}], g: [{x,y}], b: [{x,y}] }
|
|
452
|
+
* Returns pre-wrapped F3 entries for the Primary corrector.
|
|
453
|
+
*/
|
|
454
|
+
function buildCustomCurveParams(colorParams) {
|
|
455
|
+
const entries = [];
|
|
456
|
+
if (!colorParams.customCurves) return entries;
|
|
457
|
+
const curves = colorParams.customCurves;
|
|
458
|
+
const channels = [
|
|
459
|
+
{ key: 'y', metaId: PARAM_IDS.CURVES_Y_META, splineId: PARAM_IDS.CURVES_Y_SPLINE },
|
|
460
|
+
{ key: 'r', metaId: PARAM_IDS.CURVES_R_META, splineId: PARAM_IDS.CURVES_R_SPLINE },
|
|
461
|
+
{ key: 'g', metaId: PARAM_IDS.CURVES_G_META, splineId: PARAM_IDS.CURVES_G_SPLINE },
|
|
462
|
+
{ key: 'b', metaId: PARAM_IDS.CURVES_B_META, splineId: PARAM_IDS.CURVES_B_SPLINE },
|
|
463
|
+
];
|
|
464
|
+
|
|
465
|
+
// Training evidence (curve_y_scurve_1.1.2.drx): Resolve ALWAYS writes all 4 channels.
|
|
466
|
+
// Untouched channels get identity splines. All 4 metadata entries present.
|
|
467
|
+
// Determine which channels have user data vs identity
|
|
468
|
+
const hasAny = channels.some(({ key }) => curves[key]?.length > 0);
|
|
469
|
+
if (!hasAny) return entries;
|
|
470
|
+
|
|
471
|
+
// Identity curve: two points at (0,0) and (1,1) in normalized space → (0,0) and (1023,1023) after scaling
|
|
472
|
+
const IDENTITY_POINTS = [{ x: 0, y: 0 }, { x: 1, y: 1 }];
|
|
473
|
+
|
|
474
|
+
for (const { key, metaId, splineId } of channels) {
|
|
475
|
+
const pts = curves[key]?.length > 0 ? curves[key] : IDENTITY_POINTS;
|
|
476
|
+
// Metadata entry — F2 value = point count including sentinel + endpoint
|
|
477
|
+
entries.push(protoBytes(3, Buffer.concat([
|
|
478
|
+
protoVarint(1, metaId),
|
|
479
|
+
protoBytes(2, encodeCurveMeta(pts.length + 2)),
|
|
480
|
+
])));
|
|
481
|
+
// Spline data entry
|
|
482
|
+
entries.push(protoBytes(3, Buffer.concat([
|
|
483
|
+
protoVarint(1, splineId),
|
|
484
|
+
protoBytes(2, encodeCurveSpline(pts)),
|
|
485
|
+
])));
|
|
486
|
+
const isIdentity = !curves[key]?.length;
|
|
487
|
+
debugLog('[DRX] Custom Curve ' + key.toUpperCase() + ': ' + pts.length + ' points' + (isIdentity ? ' (identity)' : ''));
|
|
488
|
+
}
|
|
489
|
+
return entries;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Build HSL Curve parameters.
|
|
494
|
+
* Input: colorParams.hslCurves = { hueVsHue: [{x,y}], hueVsSat: [{x,y}], ... }
|
|
495
|
+
*/
|
|
496
|
+
function buildHSLCurveParams(colorParams) {
|
|
497
|
+
const entries = [];
|
|
498
|
+
if (!colorParams.hslCurves) return entries;
|
|
499
|
+
const hsl = colorParams.hslCurves;
|
|
500
|
+
const types = [
|
|
501
|
+
{ key: 'hueVsHue', splineId: PARAM_IDS.HSL_HUE_VS_HUE_SPLINE, metaId: drxParams.HSL_CURVES.HUE_VS_HUE_META },
|
|
502
|
+
{ key: 'hueVsSat', splineId: PARAM_IDS.HSL_HUE_VS_SAT_SPLINE, metaId: drxParams.HSL_CURVES.HUE_VS_SAT_META },
|
|
503
|
+
{ key: 'hueVsLum', splineId: PARAM_IDS.HSL_HUE_VS_LUM_SPLINE, metaId: drxParams.HSL_CURVES.HUE_VS_LUM_META },
|
|
504
|
+
{ key: 'lumVsSat', splineId: PARAM_IDS.HSL_LUM_VS_SAT_SPLINE, metaId: drxParams.HSL_CURVES.LUM_VS_SAT_META },
|
|
505
|
+
{ key: 'satVsSat', splineId: PARAM_IDS.HSL_SAT_VS_SAT_SPLINE, metaId: drxParams.HSL_CURVES.SAT_VS_SAT_META },
|
|
506
|
+
{ key: 'satVsLum', splineId: PARAM_IDS.HSL_SAT_VS_LUM_SPLINE, metaId: drxParams.HSL_CURVES.SAT_VS_LUM_META },
|
|
507
|
+
];
|
|
508
|
+
let hasAny = false;
|
|
509
|
+
// Per-curve meta values observed in LIVE R21 fixtures vary by curve (hueVsHue → 0,
|
|
510
|
+
// satVsSat → 2; the old always-6 came from older training samples). Semantics not fully
|
|
511
|
+
// decoded — callers can override per curve via colorParams.hslCurveMeta = {satVsSat: 2}.
|
|
512
|
+
const metaOverrides = colorParams.hslCurveMeta || {};
|
|
513
|
+
for (const { key, splineId, metaId } of types) {
|
|
514
|
+
const pts = hsl[key];
|
|
515
|
+
if (!pts || pts.length === 0) continue;
|
|
516
|
+
hasAny = true;
|
|
517
|
+
const metaVal = metaOverrides[key] !== undefined ? metaOverrides[key] : 6;
|
|
518
|
+
entries.push(protoBytes(3, Buffer.concat([
|
|
519
|
+
protoVarint(1, metaId),
|
|
520
|
+
protoBytes(2, encodeCurveMeta(metaVal)),
|
|
521
|
+
])));
|
|
522
|
+
// Spline data — HSL uses raw float coordinate space (NOT 0-1023)
|
|
523
|
+
// X = hue fraction (0.0-1.0 with wrap), Y = value (0.5 = neutral)
|
|
524
|
+
entries.push(protoBytes(3, Buffer.concat([
|
|
525
|
+
protoVarint(1, splineId),
|
|
526
|
+
protoBytes(2, encodeHSLCurveSpline(pts)),
|
|
527
|
+
])));
|
|
528
|
+
debugLog('[DRX] HSL Curve ' + key + ': ' + pts.length + ' control points (float space)');
|
|
529
|
+
}
|
|
530
|
+
if (hasAny) {
|
|
531
|
+
// Common flag (always {F2: 2} when any HSL curve is active)
|
|
532
|
+
entries.push(protoBytes(3, Buffer.concat([
|
|
533
|
+
protoVarint(1, PARAM_IDS.HSL_COMMON_FLAG),
|
|
534
|
+
protoBytes(2, protoVarint(2, 2)),
|
|
535
|
+
])));
|
|
536
|
+
}
|
|
537
|
+
return entries;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Create a basic node structure
|
|
542
|
+
*
|
|
543
|
+
* @param {number} nodeId - Unique node ID
|
|
544
|
+
* @param {number} xPos - X position in node graph
|
|
545
|
+
* @param {number} yPos - Y position in node graph
|
|
546
|
+
* @param {Object} colorParams - Color grading parameters
|
|
547
|
+
* @param {Object} options - Additional options (label, enabled, correctorTypes)
|
|
548
|
+
*/
|
|
549
|
+
function createNode(nodeId, xPos, yPos, colorParams = null, options = {}) {
|
|
550
|
+
const { label = '', enabled = true, nodeIndex = nodeId } = options;
|
|
551
|
+
|
|
552
|
+
// Build node header with fields in order: F1, F2, F4, F5, F6, F7?, F8
|
|
553
|
+
// Native Resolve analysis shows: id(F1), index(F2), pos(F4,F5), label?(F6), F8=44
|
|
554
|
+
// F7 (enabled) only appears when disabled - not included for enabled nodes
|
|
555
|
+
const parts = [
|
|
556
|
+
protoVarint(1, nodeId), // F1: Node ID (unique identifier, e.g., 36, 37)
|
|
557
|
+
protoVarint(2, nodeIndex), // F2: Node index (sequential, e.g., 1, 2)
|
|
558
|
+
protoVarint(4, xPos), // F4: X position
|
|
559
|
+
protoVarint(5, yPos), // F5: Y position
|
|
560
|
+
];
|
|
561
|
+
|
|
562
|
+
// Add label if provided (F6 comes before F7/F8)
|
|
563
|
+
if (label) {
|
|
564
|
+
parts.push(protoBytes(6, Buffer.from(label, 'utf-8')));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// F7 = 1 for ENABLED nodes (native Resolve analysis shows F7=1 when enabled)
|
|
568
|
+
// F7 = 0 for DISABLED nodes
|
|
569
|
+
parts.push(protoVarint(7, enabled ? 1 : 0));
|
|
570
|
+
|
|
571
|
+
parts.push(protoVarint(8, 44)); // F8: Unknown flag (always 44)
|
|
572
|
+
|
|
573
|
+
// Build corrector structure matching Resolve's native format
|
|
574
|
+
// Resolve uses:
|
|
575
|
+
// F9.F1[] = actual corrector blocks with parameters
|
|
576
|
+
// F9.F2 = compact byte array of presence marker types [3, 4, 5, 6, 18]
|
|
577
|
+
debugLog('[DRX] createNode called with colorParams:', colorParams ? 'present' : 'null');
|
|
578
|
+
const correctorBlocks = []; // F1 entries (actual correctors with params)
|
|
579
|
+
const presenceTypes = []; // Types for compact F2 presence list
|
|
580
|
+
|
|
581
|
+
if (colorParams) {
|
|
582
|
+
debugLog('[DRX] colorParams keys:', Object.keys(colorParams));
|
|
583
|
+
|
|
584
|
+
// Type 1: Primary Corrector - Lift/Gamma/Gain/Offset/Saturation
|
|
585
|
+
// Only include if there are actual params (Resolve omits neutral Primary)
|
|
586
|
+
const primaryParams = buildPrimaryCorrectorParams(colorParams);
|
|
587
|
+
debugLog('[DRX] Primary params built, length:', primaryParams.length);
|
|
588
|
+
|
|
589
|
+
// Type 2: Contrast Corrector - ALWAYS include as F1 block (Resolve pattern)
|
|
590
|
+
// Even when neutral, it gets a presence marker param, NOT the F2 list
|
|
591
|
+
const contrastParams = buildContrastCorrectorParams(colorParams);
|
|
592
|
+
debugLog('[DRX] Contrast params built, length:', contrastParams.length);
|
|
593
|
+
|
|
594
|
+
// Custom Curves (YRGB) — spline data goes in Primary corrector as additional F3 entries
|
|
595
|
+
const curveParams = buildCustomCurveParams(colorParams);
|
|
596
|
+
if (curveParams.length > 0) {
|
|
597
|
+
primaryParams.push(...curveParams);
|
|
598
|
+
debugLog('[DRX] Added', curveParams.length, 'custom curve entries to Primary corrector');
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// HSL Curves — spline data also goes in Primary corrector
|
|
602
|
+
const hslParams = buildHSLCurveParams(colorParams);
|
|
603
|
+
if (hslParams.length > 0) {
|
|
604
|
+
primaryParams.push(...hslParams);
|
|
605
|
+
debugLog('[DRX] Added', hslParams.length, 'HSL curve entries to Primary corrector');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// HDR zones — ALL zones share ONE ZONE_ADJUSTMENTS (0x86000305) param in the
|
|
609
|
+
// Primary corrector, as repeated F16.F1[] zone sub-messages (wire-confirmed on the
|
|
610
|
+
// hdr-zones-grid fixture: 5 zones in a single param, same ct1 block as other params).
|
|
611
|
+
// The old code emitted one param per zone in SEPARATE corrector blocks, so any grade
|
|
612
|
+
// with 2+ zones silently lost all but the last on decode/apply.
|
|
613
|
+
const hdrParams = buildHDRWheelParams(colorParams);
|
|
614
|
+
if (hdrParams.length > 0) {
|
|
615
|
+
const zoneMsgs = hdrParams
|
|
616
|
+
.filter((z) => z.isNested && z.nestedData)
|
|
617
|
+
.map((z) => protoBytes(1, z.nestedData));
|
|
618
|
+
if (zoneMsgs.length > 0) {
|
|
619
|
+
const field16Wrapper = protoBytes(16, Buffer.concat(zoneMsgs));
|
|
620
|
+
const paramEntry = Buffer.concat([
|
|
621
|
+
protoVarint(1, hdrParams[0].paramId),
|
|
622
|
+
protoBytes(2, field16Wrapper),
|
|
623
|
+
]);
|
|
624
|
+
primaryParams.push(protoBytes(3, paramEntry));
|
|
625
|
+
debugLog('[DRX] Added', zoneMsgs.length, 'HDR zone(s) in one ZONE_ADJUSTMENTS param');
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// HDR zone DEFINITIONS (0x86000306) — custom zone boundary (Max Range) + falloff.
|
|
630
|
+
// Structure decoded 2026-07-03 by two-point capture sweep (see
|
|
631
|
+
// test/hdr-zone-definitions.test.mjs): param value = repeated F17 { F1: record },
|
|
632
|
+
// record = { F1 name (str) · F2 DEFAULT boundary f32 · F3 CURRENT boundary f32 ·
|
|
633
|
+
// F4 DEFAULT falloff f32 · F5 CURRENT falloff f32 }. Defaults ship in native files.
|
|
634
|
+
// STOCK table verification status: Dark [-1.5, 0.2] is capture-VERIFIED; Shadow/
|
|
635
|
+
// Light falloffs 0.22 were read off the panel; the remaining boundaries are
|
|
636
|
+
// UNVERIFIED placeholders — pass explicit defaultBoundary/defaultFalloff for
|
|
637
|
+
// non-Dark zones when display fidelity of the zone editor matters. Only F3/F5
|
|
638
|
+
// (the CURRENT values) affect rendering.
|
|
639
|
+
if (colorParams && Array.isArray(colorParams.hdrZoneDefinitions) && colorParams.hdrZoneDefinitions.length) {
|
|
640
|
+
const STOCK = { Black: [-4.0, 0.2], Dark: [-1.5, 0.2], Shadow: [0.0, 0.22], Light: [0.0, 0.22], Highlight: [2.0, 0.25], Specular: [4.0, 0.25] };
|
|
641
|
+
const f32 = (tag, v) => { const b = Buffer.alloc(5); b[0] = tag; b.writeFloatLE(v, 1); return b; };
|
|
642
|
+
const zoneMsgs = [];
|
|
643
|
+
for (const z of colorParams.hdrZoneDefinitions) {
|
|
644
|
+
if (!z || !z.name) continue;
|
|
645
|
+
const stock = STOCK[z.name] || [z.boundary ?? 0, z.falloff ?? 0.2];
|
|
646
|
+
const defB = z.defaultBoundary ?? stock[0];
|
|
647
|
+
const defF = z.defaultFalloff ?? stock[1];
|
|
648
|
+
const name = Buffer.from(String(z.name), 'utf8');
|
|
649
|
+
const rec = Buffer.concat([
|
|
650
|
+
Buffer.from([0x0a, name.length]), name,
|
|
651
|
+
f32(0x15, defB),
|
|
652
|
+
f32(0x1d, z.boundary ?? defB),
|
|
653
|
+
f32(0x25, defF),
|
|
654
|
+
f32(0x2d, z.falloff ?? defF),
|
|
655
|
+
]);
|
|
656
|
+
zoneMsgs.push(protoBytes(17, protoBytes(1, rec)));
|
|
657
|
+
}
|
|
658
|
+
if (zoneMsgs.length) {
|
|
659
|
+
primaryParams.push(protoBytes(3, Buffer.concat([
|
|
660
|
+
protoVarint(1, drxParams.HDR_ZONE.ZONE_DEFINITIONS),
|
|
661
|
+
protoBytes(2, Buffer.concat(zoneMsgs)),
|
|
662
|
+
])));
|
|
663
|
+
debugLog('[DRX] Added', zoneMsgs.length, 'HDR zone DEFINITION record(s)');
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// Add Primary corrector block if we have params
|
|
668
|
+
if (primaryParams.length > 0) {
|
|
669
|
+
correctorBlocks.push(buildCorrectorBlock(CORRECTOR_TYPES.PRIMARY, primaryParams));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// COMPANION BLOCK + PRESENCE MARKERS
|
|
673
|
+
// Training evidence: Native Resolve DRX exports with curves have NO companion block
|
|
674
|
+
// (Type 2 with 0x88300001). Only Primary (Type 1) + F2 presence markers.
|
|
675
|
+
// The companion block causes errant L.Mix=100 when combined with curve splines
|
|
676
|
+
// and complete desaturation when combined with HSL curves.
|
|
677
|
+
// For non-curve grades (CDL, wheels, etc.) the companion block is harmless but unnecessary.
|
|
678
|
+
const hasCurves = curveParams.length > 0 || hslParams.length > 0;
|
|
679
|
+
if (primaryParams.length > 0 && !hasCurves) {
|
|
680
|
+
// Companion block: corrector type 2 with param 0x88300001 = {F2: 0}
|
|
681
|
+
// Only include for non-curve grades (matches Resolve behavior for simple CDL grades)
|
|
682
|
+
const companionParam = protoBytes(3, Buffer.concat([
|
|
683
|
+
protoVarint(1, 2284847105), // 0x88300001
|
|
684
|
+
protoBytes(2, protoVarint(2, 0)),
|
|
685
|
+
]));
|
|
686
|
+
correctorBlocks.push(buildCorrectorBlock(2, [companionParam]));
|
|
687
|
+
}
|
|
688
|
+
if (primaryParams.length > 0) {
|
|
689
|
+
// Presence marker list: types 3, 4, 5, 6, 18 (always present per training evidence)
|
|
690
|
+
presenceTypes.push(3, 4, 5, 6, 18);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (contrastParams.length > 0) {
|
|
694
|
+
correctorBlocks.push(buildCorrectorBlock(CORRECTOR_TYPES.CONTRAST, contrastParams));
|
|
695
|
+
}
|
|
696
|
+
// NOTE: Do NOT add presence marker corrector blocks when there are no contrast params.
|
|
697
|
+
// Presence markers cause Resolve to show qualifier/sub-corrector panel indicators,
|
|
698
|
+
// making nodes appear to have HSL qualifiers set when they don't.
|
|
699
|
+
// Resolve adds its own presence markers when it imports the grade.
|
|
700
|
+
|
|
701
|
+
// (HDR zones are folded into primaryParams above — single ZONE_ADJUSTMENTS param.)
|
|
702
|
+
} else {
|
|
703
|
+
debugLog('[DRX] No colorParams - using presence marker for Contrast');
|
|
704
|
+
// Contrast still needs F1 block with presence marker
|
|
705
|
+
const contrastPresenceBlock = buildPresenceMarkerCorrectorBlock(CORRECTOR_TYPES.CONTRAST);
|
|
706
|
+
if (contrastPresenceBlock && contrastPresenceBlock.length > 0) {
|
|
707
|
+
correctorBlocks.push(contrastPresenceBlock);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Type 3: Saturation vs Saturation
|
|
712
|
+
if (colorParams) {
|
|
713
|
+
const satVsSatParams = buildSatVsSatCorrectorParams(colorParams);
|
|
714
|
+
if (satVsSatParams.length > 0) {
|
|
715
|
+
correctorBlocks.push(buildCorrectorBlock(CORRECTOR_TYPES.SATURATION, satVsSatParams));
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Type 4: Hue Corrector
|
|
720
|
+
if (colorParams) {
|
|
721
|
+
const hueParams = buildHueCorrectorParams(colorParams);
|
|
722
|
+
if (hueParams.length > 0) {
|
|
723
|
+
correctorBlocks.push(buildCorrectorBlock(CORRECTOR_TYPES.HUE, hueParams));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Type 5: Luma vs Saturation
|
|
728
|
+
if (colorParams) {
|
|
729
|
+
const lumMixParams = buildLumMixCorrectorParams(colorParams);
|
|
730
|
+
if (lumMixParams.length > 0) {
|
|
731
|
+
correctorBlocks.push(buildCorrectorBlock(CORRECTOR_TYPES.LUM_MIX, lumMixParams));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Type 2 (HSL Qualifier), Type 4 (Power Window), Type 9 (Matte Finesse).
|
|
736
|
+
// Wired in Session 25 after extractQualifier/extractPowerWindow/
|
|
737
|
+
// extractMatteFinesse (P1.3/P1.4/P1.5) had to use synthesized inputs
|
|
738
|
+
// because these emissions never reached createNode. Each is gated on
|
|
739
|
+
// the corresponding param block being non-empty.
|
|
740
|
+
if (colorParams) {
|
|
741
|
+
// One qualifier per node (Resolve has a single qualifier with a mode). Precedence:
|
|
742
|
+
// HSL (`qualifier`) > RGB (`rgbQualifier`) > luma (`lumaQualifier`). RGB/luma wired
|
|
743
|
+
// 2026-07-02 — the builders existed with live-corrected ids but were unreachable.
|
|
744
|
+
let qualParams = buildQualifierParams(colorParams.qualifier);
|
|
745
|
+
if (qualParams.length === 0) qualParams = buildRGBQualifierParams(colorParams.rgbQualifier);
|
|
746
|
+
if (qualParams.length === 0) qualParams = buildLumaQualifierParams(colorParams.lumaQualifier);
|
|
747
|
+
if (qualParams.length > 0) {
|
|
748
|
+
correctorBlocks.push(buildCorrectorBlock(CORRECTOR_TYPES.QUALIFIER, qualParams));
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (colorParams) {
|
|
752
|
+
const win = buildWindowParams(colorParams.window);
|
|
753
|
+
if (win.transform.length > 0) {
|
|
754
|
+
correctorBlocks.push(buildCorrectorBlock(CORRECTOR_TYPES.POWER_WINDOW, win.transform));
|
|
755
|
+
}
|
|
756
|
+
if (win.softMask.length > 0) {
|
|
757
|
+
// Linear-window softness mask (0x0870xxxx) lives under corrector type 3 in live data.
|
|
758
|
+
correctorBlocks.push(buildCorrectorBlock(CORRECTOR_TYPES.WINDOW_SOFTNESS, win.softMask));
|
|
759
|
+
}
|
|
760
|
+
if (win.gradient.length > 0) {
|
|
761
|
+
// Gradient window (0x08F0xxxx) lives under compound corrector type 65554.
|
|
762
|
+
correctorBlocks.push(buildCorrectorBlock(CORRECTOR_TYPES.GRADIENT_WINDOW, win.gradient));
|
|
763
|
+
}
|
|
764
|
+
if (win.shape.length > 0) {
|
|
765
|
+
// Polygon/curve freeform shape (0x08D0xxxx vertex rings) lives under corrector
|
|
766
|
+
// type 6 in live data (the registry's ct6 "OFFSET" label predates that finding).
|
|
767
|
+
correctorBlocks.push(buildCorrectorBlock(6, win.shape));
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (colorParams && (colorParams.matteFinesse || colorParams.key)) {
|
|
771
|
+
// ct9 hosts BOTH Matte Finesse (0x0C30002x, DRX = UI/100) and the Key palette
|
|
772
|
+
// (0x0C30001x, identity — live-swept 2026-07-02). One shared block.
|
|
773
|
+
const { MATTE_FINESSE, KEY_PALETTE } = drxParams;
|
|
774
|
+
const mf = colorParams.matteFinesse || {};
|
|
775
|
+
const mfMap = [
|
|
776
|
+
[MATTE_FINESSE.DENOISE, mf.denoise],
|
|
777
|
+
[MATTE_FINESSE.BLACK_CLIP, mf.blackClip],
|
|
778
|
+
[MATTE_FINESSE.WHITE_CLIP, mf.whiteClip],
|
|
779
|
+
[MATTE_FINESSE.IN_OUT_RATIO, mf.inOutRatio],
|
|
780
|
+
[MATTE_FINESSE.CLEAN_BLACK, mf.cleanBlack],
|
|
781
|
+
[MATTE_FINESSE.CLEAN_WHITE, mf.cleanWhite],
|
|
782
|
+
[MATTE_FINESSE.MORPH_RADIUS, mf.morphRadius],
|
|
783
|
+
[MATTE_FINESSE.PRE_FILTER, mf.preFilter],
|
|
784
|
+
[MATTE_FINESSE.POST_FILTER, mf.postFilter],
|
|
785
|
+
[MATTE_FINESSE.SHADOW, mf.shadow],
|
|
786
|
+
[MATTE_FINESSE.MIDTONE, mf.midtone],
|
|
787
|
+
[MATTE_FINESSE.HIGHLIGHT, mf.highlight],
|
|
788
|
+
];
|
|
789
|
+
const ct9Params = [];
|
|
790
|
+
for (const [id, uiVal] of mfMap) {
|
|
791
|
+
if (uiVal !== undefined) ct9Params.push(createParameterEntry(id, uiVal / 100));
|
|
792
|
+
}
|
|
793
|
+
const key = colorParams.key || {};
|
|
794
|
+
const keyMap = [
|
|
795
|
+
[KEY_PALETTE.INPUT_GAIN, key.inputGain, 1],
|
|
796
|
+
[KEY_PALETTE.INPUT_OFFSET, key.inputOffset, 0],
|
|
797
|
+
[KEY_PALETTE.OUTPUT_GAIN, key.outputGain, 1],
|
|
798
|
+
[KEY_PALETTE.OUTPUT_OFFSET, key.outputOffset, 0],
|
|
799
|
+
];
|
|
800
|
+
for (const [id, uiVal, def] of keyMap) {
|
|
801
|
+
if (uiVal !== undefined && uiVal !== def) ct9Params.push(createParameterEntry(id, uiVal));
|
|
802
|
+
}
|
|
803
|
+
if (ct9Params.length > 0) {
|
|
804
|
+
correctorBlocks.push(buildCorrectorBlock(CORRECTOR_TYPES.MATTE_FINESSE, ct9Params));
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Blur palette (ct1 scalars, live-swept 2026-07-02): radius/hvRatio stored (UI−0.5)×2,
|
|
809
|
+
// scaling identity. Input: colorParams.blur = { radius, hvRatio, scaling } (UI units,
|
|
810
|
+
// 0.5-neutral for radius/hvRatio, 0.25 default for scaling). Written per-RGB (linked).
|
|
811
|
+
if (colorParams && colorParams.blur) {
|
|
812
|
+
const { BLUR_PALETTE } = drxParams;
|
|
813
|
+
const b = colorParams.blur;
|
|
814
|
+
const blurParams = [];
|
|
815
|
+
if (b.radius !== undefined && b.radius !== 0.5) {
|
|
816
|
+
const v = (b.radius - 0.5) * 2;
|
|
817
|
+
for (const id of [BLUR_PALETTE.RADIUS_R, BLUR_PALETTE.RADIUS_G, BLUR_PALETTE.RADIUS_B]) blurParams.push(createParameterEntry(id, v));
|
|
818
|
+
}
|
|
819
|
+
if (b.hvRatio !== undefined && b.hvRatio !== 0.5) {
|
|
820
|
+
const v = (b.hvRatio - 0.5) * 2;
|
|
821
|
+
for (const id of [BLUR_PALETTE.HV_RATIO_R, BLUR_PALETTE.HV_RATIO_G, BLUR_PALETTE.HV_RATIO_B]) blurParams.push(createParameterEntry(id, v));
|
|
822
|
+
}
|
|
823
|
+
if (b.scaling !== undefined && b.scaling !== 0.25) {
|
|
824
|
+
for (const id of [BLUR_PALETTE.SCALING_R, BLUR_PALETTE.SCALING_G, BLUR_PALETTE.SCALING_B]) blurParams.push(createParameterEntry(id, b.scaling));
|
|
825
|
+
}
|
|
826
|
+
if (blurParams.length > 0) {
|
|
827
|
+
correctorBlocks.push(buildCorrectorBlock(CORRECTOR_TYPES.PRIMARY, blurParams));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// Motion Effects palette (NEW corrector type ct15, live-swept 2026-07-02). Identity
|
|
832
|
+
// params write 1:1; frames writes the observed varint form (UI frames × 2 —
|
|
833
|
+
// single-point hypothesis, matches the keyframe half-frame convention).
|
|
834
|
+
// temporalMotion/motionBlur scales CONFIRMED by three-point panel capture 2026-07-03:
|
|
835
|
+
// temporalMotion: stored = 0.28×UI + 2.0 (UI 35→11.8, 60→18.8, 80→24.4 exact)
|
|
836
|
+
// motionBlur: stored = 0.0099×UI (UI 50→0.495, 25→0.2475 exact)
|
|
837
|
+
// Callers pass UI panel values; the mapping is applied here.
|
|
838
|
+
if (colorParams && colorParams.motionEffects) {
|
|
839
|
+
const { MOTION_EFFECTS } = drxParams;
|
|
840
|
+
const m = colorParams.motionEffects;
|
|
841
|
+
const meParams = [];
|
|
842
|
+
const meMap = [
|
|
843
|
+
[MOTION_EFFECTS.SPATIAL_LUMA, m.spatialLuma],
|
|
844
|
+
[MOTION_EFFECTS.SPATIAL_CHROMA, m.spatialChroma],
|
|
845
|
+
[MOTION_EFFECTS.SPATIAL_BLEND, m.spatialBlend],
|
|
846
|
+
[MOTION_EFFECTS.TEMPORAL_LUMA, m.temporalLuma],
|
|
847
|
+
[MOTION_EFFECTS.TEMPORAL_CHROMA, m.temporalChroma],
|
|
848
|
+
[MOTION_EFFECTS.TEMPORAL_BLEND, m.temporalBlend],
|
|
849
|
+
[MOTION_EFFECTS.TEMPORAL_MOTION, m.temporalMotion === undefined ? undefined : 0.28 * m.temporalMotion + 2.0],
|
|
850
|
+
[MOTION_EFFECTS.MOTION_BLUR, m.motionBlur === undefined ? undefined : 0.0099 * m.motionBlur],
|
|
851
|
+
];
|
|
852
|
+
for (const [id, uiVal] of meMap) {
|
|
853
|
+
if (uiVal !== undefined && uiVal !== 0) meParams.push(createParameterEntry(id, uiVal));
|
|
854
|
+
}
|
|
855
|
+
if (m.frames !== undefined && m.frames !== 0) {
|
|
856
|
+
meParams.push(createVarintParameterEntry(MOTION_EFFECTS.FRAMES_FLAG, m.frames * 2));
|
|
857
|
+
}
|
|
858
|
+
if (meParams.length > 0) {
|
|
859
|
+
correctorBlocks.push(buildCorrectorBlock(CORRECTOR_TYPES.MOTION_EFFECTS, meParams));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// NOTE: Types 3, 4, 5, 6, 18 are OMITTED from the F2 presence list.
|
|
864
|
+
// While native Resolve DRX files include these as markers for sub-corrector
|
|
865
|
+
// panels (Sat vs Sat, Hue, Lum vs Sat, Offset, Curves), our encoding was
|
|
866
|
+
// causing Resolve to interpret nodes as having HSL qualifiers set.
|
|
867
|
+
// The F1 corrector blocks (Primary + Contrast) are sufficient for the
|
|
868
|
+
// corrections to apply. Users can manually add sub-correctors in Resolve.
|
|
869
|
+
// If we need to encode these in the future, the wire format needs to match
|
|
870
|
+
// Resolve's native packed-varint encoding exactly.
|
|
871
|
+
|
|
872
|
+
// Build F9 node settings: F1[] for corrector blocks, F2 for presence list
|
|
873
|
+
const f9Parts = [];
|
|
874
|
+
|
|
875
|
+
// Add corrector blocks as F1 entries
|
|
876
|
+
for (const block of correctorBlocks) {
|
|
877
|
+
if (block && block.length > 0) {
|
|
878
|
+
f9Parts.push(block); // Already wrapped as F1
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Add compact presence list as F2 (raw byte array of type IDs)
|
|
883
|
+
if (presenceTypes.length > 0) {
|
|
884
|
+
const presenceBytes = Buffer.from(presenceTypes);
|
|
885
|
+
f9Parts.push(protoBytes(2, presenceBytes));
|
|
886
|
+
debugLog('[DRX] Added F2 presence list:', presenceTypes);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
debugLog('[DRX] Total F1 corrector blocks:', correctorBlocks.filter(b => b && b.length > 0).length);
|
|
890
|
+
debugLog('[DRX] Presence types in F2:', presenceTypes.length);
|
|
891
|
+
|
|
892
|
+
if (f9Parts.length > 0) {
|
|
893
|
+
const nodeSettings = protoBytes(9, Buffer.concat(f9Parts));
|
|
894
|
+
parts.push(nodeSettings);
|
|
895
|
+
} else {
|
|
896
|
+
debugLog('[DRX] WARNING: No correctors generated for this node');
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// F10: node tool list. Default = the bare node marker; when the node carries an
|
|
900
|
+
// OFX spec ({ofx:{pluginId, params, options?}}), emit the full OFX container instead
|
|
901
|
+
// (params are self-describing name/value pairs on the wire — see extract-ofx-params).
|
|
902
|
+
if (colorParams && colorParams.ofx && colorParams.ofx.pluginId) {
|
|
903
|
+
parts.push(buildOFXToolEntry(colorParams.ofx.pluginId, colorParams.ofx.params || {}, colorParams.ofx.options || {}));
|
|
904
|
+
} else {
|
|
905
|
+
// Structure: F10 = {F1 = {F1=0xC0000001, F2={F2=2}}}
|
|
906
|
+
const f10InnerInner = Buffer.concat([
|
|
907
|
+
protoVarint(1, 0xC0000001), // F1 = 0xC0000001
|
|
908
|
+
protoBytes(2, protoVarint(2, 2)), // F2 = {F2=2}
|
|
909
|
+
]);
|
|
910
|
+
const f10Outer = protoBytes(1, f10InnerInner);
|
|
911
|
+
parts.push(protoBytes(10, f10Outer));
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// F12: Timestamp - native Resolve analysis shows this IS present in nodes
|
|
915
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
916
|
+
parts.push(protoVarint(12, timestamp));
|
|
917
|
+
|
|
918
|
+
return protoBytes(7, Buffer.concat(parts));
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Build Primary Corrector parameters (Lift/Gamma/Gain/Offset)
|
|
923
|
+
*/
|
|
924
|
+
function buildPrimaryCorrectorParams(colorParams) {
|
|
925
|
+
const paramEntries = [];
|
|
926
|
+
|
|
927
|
+
debugLog('[DRX] Building primary corrector from params:', JSON.stringify(colorParams, null, 2));
|
|
928
|
+
|
|
929
|
+
// Lift (default 0 — guard against undefined to prevent NaN in protobuf)
|
|
930
|
+
if (colorParams.lift) {
|
|
931
|
+
if (colorParams.lift.r != null && colorParams.lift.r !== 0) { debugLog('[DRX] Adding lift.r:', colorParams.lift.r); paramEntries.push(createParameterEntry(PARAM_IDS.LIFT_R, colorParams.lift.r)); }
|
|
932
|
+
if (colorParams.lift.g != null && colorParams.lift.g !== 0) { debugLog('[DRX] Adding lift.g:', colorParams.lift.g); paramEntries.push(createParameterEntry(PARAM_IDS.LIFT_G, colorParams.lift.g)); }
|
|
933
|
+
if (colorParams.lift.b != null && colorParams.lift.b !== 0) { debugLog('[DRX] Adding lift.b:', colorParams.lift.b); paramEntries.push(createParameterEntry(PARAM_IDS.LIFT_B, colorParams.lift.b)); }
|
|
934
|
+
if (colorParams.lift.master != null && colorParams.lift.master !== 0) { debugLog('[DRX] Adding lift.master:', colorParams.lift.master); paramEntries.push(createParameterEntry(PARAM_IDS.LIFT_MASTER, colorParams.lift.master)); }
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// Gain (default 1.0 — guard against undefined)
|
|
938
|
+
if (colorParams.gain) {
|
|
939
|
+
if (colorParams.gain.r != null && colorParams.gain.r !== 1) { debugLog('[DRX] Adding gain.r:', colorParams.gain.r); paramEntries.push(createParameterEntry(PARAM_IDS.GAIN_R, colorParams.gain.r)); }
|
|
940
|
+
if (colorParams.gain.g != null && colorParams.gain.g !== 1) { debugLog('[DRX] Adding gain.g:', colorParams.gain.g); paramEntries.push(createParameterEntry(PARAM_IDS.GAIN_G, colorParams.gain.g)); }
|
|
941
|
+
if (colorParams.gain.b != null && colorParams.gain.b !== 1) { debugLog('[DRX] Adding gain.b:', colorParams.gain.b); paramEntries.push(createParameterEntry(PARAM_IDS.GAIN_B, colorParams.gain.b)); }
|
|
942
|
+
if (colorParams.gain.master != null && colorParams.gain.master !== 1) { debugLog('[DRX] Adding gain.master:', colorParams.gain.master); paramEntries.push(createParameterEntry(PARAM_IDS.GAIN_MASTER, colorParams.gain.master)); }
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Gamma (default 0 — guard against undefined)
|
|
946
|
+
if (colorParams.gamma) {
|
|
947
|
+
if (colorParams.gamma.r != null && colorParams.gamma.r !== 0) paramEntries.push(createParameterEntry(PARAM_IDS.GAMMA_R, colorParams.gamma.r));
|
|
948
|
+
if (colorParams.gamma.g != null && colorParams.gamma.g !== 0) paramEntries.push(createParameterEntry(PARAM_IDS.GAMMA_G, colorParams.gamma.g));
|
|
949
|
+
if (colorParams.gamma.b != null && colorParams.gamma.b !== 0) paramEntries.push(createParameterEntry(PARAM_IDS.GAMMA_B, colorParams.gamma.b));
|
|
950
|
+
if (colorParams.gamma.master != null && colorParams.gamma.master !== 0) paramEntries.push(createParameterEntry(PARAM_IDS.GAMMA_MASTER, colorParams.gamma.master));
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// Offset (default 0)
|
|
954
|
+
if (colorParams.offset) {
|
|
955
|
+
if (colorParams.offset.r != null && colorParams.offset.r !== 0) paramEntries.push(createParameterEntry(PARAM_IDS.OFFSET_R, colorParams.offset.r));
|
|
956
|
+
if (colorParams.offset.g != null && colorParams.offset.g !== 0) paramEntries.push(createParameterEntry(PARAM_IDS.OFFSET_G, colorParams.offset.g));
|
|
957
|
+
if (colorParams.offset.b != null && colorParams.offset.b !== 0) paramEntries.push(createParameterEntry(PARAM_IDS.OFFSET_B, colorParams.offset.b));
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Saturation (Resolve Primaries UI: 0-100, unity=50)
|
|
961
|
+
// DRX ENCODING: float multiplier where 1.0 = unity (50 on UI)
|
|
962
|
+
// Conversion: UI value / 50 = DRX float (0→0.0, 50→1.0, 70→1.4, 100→2.0)
|
|
963
|
+
// Discovered via DRX AutoResearch 2026-03-17: manual Sat 70 in Resolve → 1.4 in DRX body.
|
|
964
|
+
if (colorParams.saturation !== undefined && colorParams.saturation !== 50) {
|
|
965
|
+
const satFloat = colorParams.saturation / 50;
|
|
966
|
+
debugLog('[DRX] Adding saturation:', colorParams.saturation, '→ DRX float:', satFloat);
|
|
967
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SATURATION_PRIMARY, satFloat));
|
|
968
|
+
} else {
|
|
969
|
+
debugLog('[DRX] Skipping saturation - value:', colorParams.saturation, '(unity=50)');
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Hue Rotate (0-100 UI, default 50, DRX encoding: (UI - 50) / 50)
|
|
973
|
+
if (colorParams.hueRotate !== undefined && colorParams.hueRotate !== 50) {
|
|
974
|
+
const hueFloat = (colorParams.hueRotate - 50) / 50;
|
|
975
|
+
debugLog('[DRX] Adding hueRotate:', colorParams.hueRotate, '→ DRX float:', hueFloat);
|
|
976
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.HUE_ROTATE, hueFloat));
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Temperature (default 0)
|
|
980
|
+
if (colorParams.temperature !== undefined && colorParams.temperature !== 0) {
|
|
981
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.TEMPERATURE, colorParams.temperature));
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Tint (default 0)
|
|
985
|
+
if (colorParams.tint !== undefined && colorParams.tint !== 0) {
|
|
986
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.TINT, colorParams.tint));
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Midtone Detail (default 0)
|
|
990
|
+
// Scaling happens in parseAdjustments (simple mode ×100, direct mode pass-through)
|
|
991
|
+
if (colorParams.midtoneDetail !== undefined && colorParams.midtoneDetail !== 0) {
|
|
992
|
+
debugLog('[DRX] Adding midtoneDetail:', colorParams.midtoneDetail);
|
|
993
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.MIDTONE_DETAIL, colorParams.midtoneDetail));
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// Contrast (default 1.0)
|
|
997
|
+
if (colorParams.contrast !== undefined && colorParams.contrast !== 1) {
|
|
998
|
+
debugLog('[DRX] Adding contrast to Primary block via LOG_CONTRAST:', colorParams.contrast);
|
|
999
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.LOG_CONTRAST, colorParams.contrast));
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Pivot (default 0.435)
|
|
1003
|
+
if (colorParams.pivot !== undefined && colorParams.pivot !== 0.435) {
|
|
1004
|
+
debugLog('[DRX] Adding pivot to Primary block:', colorParams.pivot);
|
|
1005
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.PIVOT_PRIMARY, colorParams.pivot));
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// Color Boost (default 0)
|
|
1009
|
+
if (colorParams.colorBoost !== undefined && colorParams.colorBoost !== 0) {
|
|
1010
|
+
debugLog('[DRX] Adding colorBoost to Primary block:', colorParams.colorBoost);
|
|
1011
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.COLOR_BOOST, colorParams.colorBoost));
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Soft Clip High (default 1.0)
|
|
1015
|
+
if (colorParams.softClipHigh !== undefined && colorParams.softClipHigh !== 1) {
|
|
1016
|
+
const scVal = colorParams.softClipHigh;
|
|
1017
|
+
debugLog('[DRX] Adding softClipHigh R/G/B to Primary block:', scVal);
|
|
1018
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SOFT_CLIP_HIGH_R, scVal));
|
|
1019
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SOFT_CLIP_HIGH_G, scVal));
|
|
1020
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SOFT_CLIP_HIGH_B, scVal));
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Soft Clip Low (default 0)
|
|
1024
|
+
if (colorParams.softClipLow !== undefined && colorParams.softClipLow !== 0) {
|
|
1025
|
+
const scVal = colorParams.softClipLow;
|
|
1026
|
+
debugLog('[DRX] Adding softClipLow R/G/B to Primary block:', scVal);
|
|
1027
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SOFT_CLIP_LOW_R, scVal));
|
|
1028
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SOFT_CLIP_LOW_G, scVal));
|
|
1029
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SOFT_CLIP_LOW_B, scVal));
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Soft Clip Low Soft (default 0, encoding: UI / 50)
|
|
1033
|
+
if (colorParams.softClipLowSoft !== undefined && colorParams.softClipLowSoft !== 0) {
|
|
1034
|
+
const scVal = colorParams.softClipLowSoft / 50;
|
|
1035
|
+
debugLog('[DRX] Adding softClipLowSoft R/G/B:', colorParams.softClipLowSoft, '→', scVal);
|
|
1036
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SOFT_CLIP_LOW_SOFT_R, scVal));
|
|
1037
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SOFT_CLIP_LOW_SOFT_G, scVal));
|
|
1038
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SOFT_CLIP_LOW_SOFT_B, scVal));
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// Soft Clip High Soft (default 0, encoding: UI / 50)
|
|
1042
|
+
if (colorParams.softClipHighSoft !== undefined && colorParams.softClipHighSoft !== 0) {
|
|
1043
|
+
const scVal = colorParams.softClipHighSoft / 50;
|
|
1044
|
+
debugLog('[DRX] Adding softClipHighSoft R/G/B:', colorParams.softClipHighSoft, '→', scVal);
|
|
1045
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SOFT_CLIP_HIGH_SOFT_R, scVal));
|
|
1046
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SOFT_CLIP_HIGH_SOFT_G, scVal));
|
|
1047
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SOFT_CLIP_HIGH_SOFT_B, scVal));
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Contrast High Range (default 0.550 in UI, INVERTED encoding: DRX = 1.0 - UI)
|
|
1051
|
+
if (colorParams.contrastHighRange !== undefined && Math.abs(colorParams.contrastHighRange - 0.55) > 0.001) {
|
|
1052
|
+
const drxVal = 1.0 - colorParams.contrastHighRange;
|
|
1053
|
+
debugLog('[DRX] Adding contrastHighRange to Primary block: UI', colorParams.contrastHighRange, '→ DRX', drxVal);
|
|
1054
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.CONTRAST_HIGH_RANGE_PRIMARY, drxVal));
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Contrast Low Range (default 0.333)
|
|
1058
|
+
if (colorParams.contrastLowRange !== undefined && Math.abs(colorParams.contrastLowRange - 0.333) > 0.001) {
|
|
1059
|
+
debugLog('[DRX] Adding contrastLowRange to Primary block:', colorParams.contrastLowRange);
|
|
1060
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.CONTRAST_LOW_RANGE_PRIMARY, colorParams.contrastLowRange));
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Black Offset (default 0)
|
|
1064
|
+
if (colorParams.blackOffset !== undefined && colorParams.blackOffset !== 0) {
|
|
1065
|
+
debugLog('[DRX] Adding blackOffset:', colorParams.blackOffset);
|
|
1066
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.HDR_BLACK_OFFSET, colorParams.blackOffset));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1070
|
+
// LOG WHEELS (offset-style grading) - correctorType: 1 (Primary)
|
|
1071
|
+
// These are part of the Primary corrector, NOT contrast corrector
|
|
1072
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1073
|
+
|
|
1074
|
+
// Log Shadow (Low)
|
|
1075
|
+
if (colorParams.logShadow) {
|
|
1076
|
+
if (colorParams.logShadow.r != null && colorParams.logShadow.r !== 0) {
|
|
1077
|
+
debugLog('[DRX] Adding logShadow.r:', colorParams.logShadow.r);
|
|
1078
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.LOG_SHADOW_R, colorParams.logShadow.r));
|
|
1079
|
+
}
|
|
1080
|
+
if (colorParams.logShadow.g != null && colorParams.logShadow.g !== 0) {
|
|
1081
|
+
debugLog('[DRX] Adding logShadow.g:', colorParams.logShadow.g);
|
|
1082
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.LOG_SHADOW_G, colorParams.logShadow.g));
|
|
1083
|
+
}
|
|
1084
|
+
if (colorParams.logShadow.b != null && colorParams.logShadow.b !== 0) {
|
|
1085
|
+
debugLog('[DRX] Adding logShadow.b:', colorParams.logShadow.b);
|
|
1086
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.LOG_SHADOW_B, colorParams.logShadow.b));
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Log Midtone (Mid)
|
|
1091
|
+
if (colorParams.logMid) {
|
|
1092
|
+
if (colorParams.logMid.r != null && colorParams.logMid.r !== 0) {
|
|
1093
|
+
debugLog('[DRX] Adding logMid.r:', colorParams.logMid.r);
|
|
1094
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.LOG_MIDTONE_R, colorParams.logMid.r));
|
|
1095
|
+
}
|
|
1096
|
+
if (colorParams.logMid.g != null && colorParams.logMid.g !== 0) {
|
|
1097
|
+
debugLog('[DRX] Adding logMid.g:', colorParams.logMid.g);
|
|
1098
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.LOG_MIDTONE_G, colorParams.logMid.g));
|
|
1099
|
+
}
|
|
1100
|
+
if (colorParams.logMid.b != null && colorParams.logMid.b !== 0) {
|
|
1101
|
+
debugLog('[DRX] Adding logMid.b:', colorParams.logMid.b);
|
|
1102
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.LOG_MIDTONE_B, colorParams.logMid.b));
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Log Highlight (High)
|
|
1107
|
+
if (colorParams.logHigh) {
|
|
1108
|
+
if (colorParams.logHigh.r != null && colorParams.logHigh.r !== 0) {
|
|
1109
|
+
debugLog('[DRX] Adding logHigh.r:', colorParams.logHigh.r);
|
|
1110
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.LOG_HIGHLIGHT_R, colorParams.logHigh.r));
|
|
1111
|
+
}
|
|
1112
|
+
if (colorParams.logHigh.g != null && colorParams.logHigh.g !== 0) {
|
|
1113
|
+
debugLog('[DRX] Adding logHigh.g:', colorParams.logHigh.g);
|
|
1114
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.LOG_HIGHLIGHT_G, colorParams.logHigh.g));
|
|
1115
|
+
}
|
|
1116
|
+
if (colorParams.logHigh.b != null && colorParams.logHigh.b !== 0) {
|
|
1117
|
+
debugLog('[DRX] Adding logHigh.b:', colorParams.logHigh.b);
|
|
1118
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.LOG_HIGHLIGHT_B, colorParams.logHigh.b));
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1123
|
+
// RGB MIXER (3x3 channel mixing matrix)
|
|
1124
|
+
// Param IDs trained 2026-03-17 from rgbmixer_1.1.2.drx
|
|
1125
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1126
|
+
if (colorParams.rgbMixer) {
|
|
1127
|
+
const mx = colorParams.rgbMixer;
|
|
1128
|
+
// Identity matrix defaults: diagonal=1.0, off-diagonal=0.0
|
|
1129
|
+
const entries = [
|
|
1130
|
+
{ id: PARAM_IDS.RGB_MIXER_RR, val: mx.rr, def: 1.0 },
|
|
1131
|
+
{ id: PARAM_IDS.RGB_MIXER_GR, val: mx.gr, def: 0.0 },
|
|
1132
|
+
{ id: PARAM_IDS.RGB_MIXER_BR, val: mx.br, def: 0.0 },
|
|
1133
|
+
{ id: PARAM_IDS.RGB_MIXER_RG, val: mx.rg, def: 0.0 },
|
|
1134
|
+
{ id: PARAM_IDS.RGB_MIXER_GG, val: mx.gg, def: 1.0 },
|
|
1135
|
+
{ id: PARAM_IDS.RGB_MIXER_BG, val: mx.bg, def: 0.0 },
|
|
1136
|
+
{ id: PARAM_IDS.RGB_MIXER_RB, val: mx.rb, def: 0.0 },
|
|
1137
|
+
{ id: PARAM_IDS.RGB_MIXER_GB, val: mx.gb, def: 0.0 },
|
|
1138
|
+
{ id: PARAM_IDS.RGB_MIXER_BB, val: mx.bb, def: 1.0 },
|
|
1139
|
+
];
|
|
1140
|
+
// Always write all 9 values when RGB mixer is specified (Resolve expects complete matrix)
|
|
1141
|
+
for (const { id, val, def } of entries) {
|
|
1142
|
+
const v = val !== undefined ? val : def;
|
|
1143
|
+
paramEntries.push(createParameterEntry(id, v));
|
|
1144
|
+
}
|
|
1145
|
+
debugLog('[DRX] RGB Mixer: RR=' + (mx.rr ?? 1) + ' GG=' + (mx.gg ?? 1) + ' BB=' + (mx.bb ?? 1));
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Highlights slider (default 0)
|
|
1149
|
+
if (colorParams.highlights !== undefined && colorParams.highlights !== 0) {
|
|
1150
|
+
debugLog('[DRX] Adding highlights:', colorParams.highlights);
|
|
1151
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.HIGHLIGHTS, colorParams.highlights));
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Shadows slider (direct value, e.g., 50)
|
|
1155
|
+
if (colorParams.shadows !== undefined && colorParams.shadows !== 0) {
|
|
1156
|
+
debugLog('[DRX] Adding shadows:', colorParams.shadows);
|
|
1157
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.SHADOWS, colorParams.shadows));
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Lum Mix slider (0.0-1.0, default 0)
|
|
1161
|
+
if (colorParams.lumMixSlider !== undefined && colorParams.lumMixSlider !== 0) {
|
|
1162
|
+
debugLog('[DRX] Adding lumMixSlider:', colorParams.lumMixSlider);
|
|
1163
|
+
paramEntries.push(createParameterEntry(PARAM_IDS.LUM_MIX_SLIDER, colorParams.lumMixSlider));
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1167
|
+
// COLORSLICE global controls (0x86000600–605, ct1) — WRITE PATH ADDED 2026-07-02.
|
|
1168
|
+
// Identity scale for all except Hue, which Resolve stores NEGATED (UI +X → −X);
|
|
1169
|
+
// calibrated live 2026-06-22. Per-vector grid (0x86000606 blob) is decode-only.
|
|
1170
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1171
|
+
if (colorParams.colorSlice) {
|
|
1172
|
+
const CS = drxParams.COLORSLICE;
|
|
1173
|
+
const cs = colorParams.colorSlice;
|
|
1174
|
+
const csMap = [
|
|
1175
|
+
[CS.DENSITY, cs.density, 0],
|
|
1176
|
+
[CS.DENSITY_DEPTH, cs.densityDepth, 0],
|
|
1177
|
+
[CS.SAT, cs.sat, 1],
|
|
1178
|
+
[CS.SAT_BALANCE, cs.satBalance, 0],
|
|
1179
|
+
[CS.SAT_DEPTH, cs.satDepth, 0],
|
|
1180
|
+
[CS.HUE, cs.hue !== undefined ? -cs.hue : undefined, 0], // NEGATED
|
|
1181
|
+
];
|
|
1182
|
+
let n = 0;
|
|
1183
|
+
for (const [id, val, def] of csMap) {
|
|
1184
|
+
if (val !== undefined && val !== def) { paramEntries.push(createParameterEntry(id, val)); n++; }
|
|
1185
|
+
}
|
|
1186
|
+
if (n) debugLog('[DRX] ColorSlice global params:', n);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1190
|
+
// COLOR WARPER (chroma-warp pin list, ct1) — WRITE PATH ADDED 2026-07-02.
|
|
1191
|
+
// Wire (RE'd live 2026-06-22): mode/config varints 0x86000133/136/137 + pins at
|
|
1192
|
+
// 0x86000138 = { F2: { F27: { F1: [ <pin>, … ] } } }; pin = { F1=id, F2/F3=src XY,
|
|
1193
|
+
// F4/F5=dst XY, F6=chromaRange, F7=exposure (omit 0), F8/F9/F10=tonal low/high/pivot }.
|
|
1194
|
+
// All floats identity-scale vs the UI Pin controls.
|
|
1195
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1196
|
+
if (colorParams.colorWarper && Array.isArray(colorParams.colorWarper.pins) && colorParams.colorWarper.pins.length > 0) {
|
|
1197
|
+
const CW = drxParams.COLOR_WARPER;
|
|
1198
|
+
const w = colorParams.colorWarper;
|
|
1199
|
+
// The live chroma-warp fixture carries configA=2 + configB=2 and NO MODE_FLAG param —
|
|
1200
|
+
// mirror it exactly (a mode override is still possible via w.mode).
|
|
1201
|
+
if (w.mode !== undefined) paramEntries.push(createVarintParameterEntry(CW.MODE_FLAG, w.mode));
|
|
1202
|
+
paramEntries.push(createVarintParameterEntry(CW.CONFIG_A, w.configA !== undefined ? w.configA : 2));
|
|
1203
|
+
paramEntries.push(createVarintParameterEntry(CW.CONFIG_B, w.configB !== undefined ? w.configB : 2));
|
|
1204
|
+
const pinMsgs = [];
|
|
1205
|
+
for (const [i, pin] of w.pins.entries()) {
|
|
1206
|
+
const parts = [protoVarint(1, pin.id !== undefined ? pin.id : i + 1)];
|
|
1207
|
+
const f32 = (fieldNum, v) => { if (typeof v === 'number') parts.push(protoFloat32(fieldNum, v)); };
|
|
1208
|
+
f32(2, pin.srcX); f32(3, pin.srcY);
|
|
1209
|
+
f32(4, pin.dstX); f32(5, pin.dstY);
|
|
1210
|
+
f32(6, pin.chromaRange);
|
|
1211
|
+
if (typeof pin.exposure === 'number' && pin.exposure !== 0) f32(7, pin.exposure);
|
|
1212
|
+
f32(8, pin.tonalLow); f32(9, pin.tonalHigh); f32(10, pin.tonalPivot);
|
|
1213
|
+
pinMsgs.push(protoBytes(1, Buffer.concat(parts)));
|
|
1214
|
+
}
|
|
1215
|
+
const envelope = protoBytes(27, Buffer.concat(pinMsgs)); // F27 = pin list container
|
|
1216
|
+
const pinsEntry = protoBytes(3, Buffer.concat([
|
|
1217
|
+
protoVarint(1, CW.PINS),
|
|
1218
|
+
protoBytes(2, envelope),
|
|
1219
|
+
]));
|
|
1220
|
+
paramEntries.push(pinsEntry);
|
|
1221
|
+
debugLog('[DRX] Color Warper pins:', w.pins.length);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
debugLog('[DRX] Primary corrector entries count:', paramEntries.length);
|
|
1225
|
+
return paramEntries;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Build Contrast Corrector parameters (Contrast, Pivot, Soft Clip)
|
|
1230
|
+
*/
|
|
1231
|
+
function buildContrastCorrectorParams(colorParams) {
|
|
1232
|
+
const paramEntries = [];
|
|
1233
|
+
|
|
1234
|
+
// NOTE: ALL params that were formerly in this Type 2 block have been moved to
|
|
1235
|
+
// buildPrimaryCorrectorParams() (Type 1). DRX AutoResearch 2026-03-17 confirmed
|
|
1236
|
+
// that Resolve stores contrast, pivot, colorBoost, softClip, and range controls
|
|
1237
|
+
// in the Primary corrector with TEMP_TINT-style param IDs. Putting them in the
|
|
1238
|
+
// Type 2 Contrast corrector causes Resolve to misinterpret the block as an
|
|
1239
|
+
// HSL Qualifier, corrupting the grade.
|
|
1240
|
+
//
|
|
1241
|
+
// This function now returns empty — the Type 2 corrector block will not be generated.
|
|
1242
|
+
debugLog('[DRX] Contrast corrector (Type 2) empty - all params in Primary (Type 1)');
|
|
1243
|
+
return paramEntries;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Build Hue Corrector parameters (Type 4).
|
|
1248
|
+
*
|
|
1249
|
+
* Hue correction adjusts hue rotation per tonal range. The 6 parameter IDs
|
|
1250
|
+
* have been discovered from DRX analysis but their semantic mapping (which
|
|
1251
|
+
* param controls which hue range) is NOT yet confirmed from training data.
|
|
1252
|
+
*
|
|
1253
|
+
* HYPOTHESIZED semantic mapping (needs training session to confirm):
|
|
1254
|
+
* PARAM_1 (139460609) — possibly master hue rotate or red hue shift
|
|
1255
|
+
* PARAM_2 (139460612) — possibly yellow or green hue shift
|
|
1256
|
+
* PARAM_3 (139460613) — possibly green or cyan hue shift
|
|
1257
|
+
* PARAM_4 (139460614) — possibly cyan or blue hue shift
|
|
1258
|
+
* PARAM_5 (139460619) — possibly blue or magenta hue shift
|
|
1259
|
+
* PARAM_6 (139460620) — possibly magenta hue shift or mix
|
|
1260
|
+
*
|
|
1261
|
+
* Usage: Pass raw param values keyed by ID until semantics are confirmed:
|
|
1262
|
+
* { hue: { [HUE.PARAM_1]: 0.5, [HUE.PARAM_3]: -0.2 } }
|
|
1263
|
+
*
|
|
1264
|
+
* @param {object} colorParams - contains `hue` object with paramId keys
|
|
1265
|
+
* @returns {Buffer[]} - encoded parameter entries
|
|
1266
|
+
*/
|
|
1267
|
+
function buildHueCorrectorParams(colorParams) {
|
|
1268
|
+
const paramEntries = [];
|
|
1269
|
+
if (!colorParams.hue) return paramEntries;
|
|
1270
|
+
|
|
1271
|
+
const hueParams = colorParams.hue;
|
|
1272
|
+
for (const [paramId, value] of Object.entries(hueParams)) {
|
|
1273
|
+
const id = Number(paramId);
|
|
1274
|
+
if (isNaN(id) || value === undefined || value === 0) continue;
|
|
1275
|
+
paramEntries.push(createParameterEntry(id, value));
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
if (paramEntries.length > 0) {
|
|
1279
|
+
debugLog('[DRX] Hue corrector entries:', paramEntries.length);
|
|
1280
|
+
}
|
|
1281
|
+
return paramEntries;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Build Luma vs Saturation parameters (Type 5).
|
|
1286
|
+
*
|
|
1287
|
+
* Controls how saturation is modulated based on luminance. The 11 parameter IDs
|
|
1288
|
+
* have been discovered but their semantic mapping is NOT yet confirmed.
|
|
1289
|
+
*
|
|
1290
|
+
* HYPOTHESIZED: These likely represent control points on a curve where:
|
|
1291
|
+
* - X axis = luminance (dark to bright)
|
|
1292
|
+
* - Y axis = saturation multiplier
|
|
1293
|
+
* - 11 points define the spline shape
|
|
1294
|
+
*
|
|
1295
|
+
* Usage: Pass raw param values keyed by ID:
|
|
1296
|
+
* { lumMix: { [LUM_MIX.PARAM_1]: 0.8, [LUM_MIX.PARAM_5]: 1.2 } }
|
|
1297
|
+
*
|
|
1298
|
+
* @param {object} colorParams - contains `lumMix` object with paramId keys
|
|
1299
|
+
* @returns {Buffer[]} - encoded parameter entries
|
|
1300
|
+
*/
|
|
1301
|
+
function buildLumMixCorrectorParams(colorParams) {
|
|
1302
|
+
const paramEntries = [];
|
|
1303
|
+
if (!colorParams.lumMix) return paramEntries;
|
|
1304
|
+
|
|
1305
|
+
const lumParams = colorParams.lumMix;
|
|
1306
|
+
for (const [paramId, value] of Object.entries(lumParams)) {
|
|
1307
|
+
const id = Number(paramId);
|
|
1308
|
+
if (isNaN(id) || value === undefined) continue;
|
|
1309
|
+
paramEntries.push(createParameterEntry(id, value));
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
if (paramEntries.length > 0) {
|
|
1313
|
+
debugLog('[DRX] LumMix corrector entries:', paramEntries.length);
|
|
1314
|
+
}
|
|
1315
|
+
return paramEntries;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* Build Saturation vs Saturation parameters (Type 3).
|
|
1320
|
+
*
|
|
1321
|
+
* Controls how saturation is modulated based on existing saturation level.
|
|
1322
|
+
* The 7 parameter IDs are discovered but semantics NOT confirmed.
|
|
1323
|
+
*
|
|
1324
|
+
* Usage: Pass raw param values keyed by ID:
|
|
1325
|
+
* { satVsSat: { [SAT_VS_SAT.PARAM_1]: 0.5 } }
|
|
1326
|
+
*
|
|
1327
|
+
* @param {object} colorParams - contains `satVsSat` object with paramId keys
|
|
1328
|
+
* @returns {Buffer[]} - encoded parameter entries
|
|
1329
|
+
*/
|
|
1330
|
+
function buildSatVsSatCorrectorParams(colorParams) {
|
|
1331
|
+
const paramEntries = [];
|
|
1332
|
+
if (!colorParams.satVsSat) return paramEntries;
|
|
1333
|
+
|
|
1334
|
+
const satParams = colorParams.satVsSat;
|
|
1335
|
+
for (const [paramId, value] of Object.entries(satParams)) {
|
|
1336
|
+
const id = Number(paramId);
|
|
1337
|
+
if (isNaN(id) || value === undefined) continue;
|
|
1338
|
+
paramEntries.push(createParameterEntry(id, value));
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
if (paramEntries.length > 0) {
|
|
1342
|
+
debugLog('[DRX] SatVsSat corrector entries:', paramEntries.length);
|
|
1343
|
+
}
|
|
1344
|
+
return paramEntries;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/**
|
|
1348
|
+
* Build a presence marker parameter entry for corrector types 2-18
|
|
1349
|
+
* These markers tell Resolve the corrector exists but is at neutral/bypass state
|
|
1350
|
+
*
|
|
1351
|
+
* Real Resolve format: F3 { F1=presenceId, F2={ F2=0 } }
|
|
1352
|
+
* Much simpler than originally thought - just a single nested F2=0
|
|
1353
|
+
*/
|
|
1354
|
+
function createPresenceMarkerEntry(correctorType) {
|
|
1355
|
+
const presenceId = PRESENCE_MARKER_IDS[correctorType];
|
|
1356
|
+
if (!presenceId) return null;
|
|
1357
|
+
|
|
1358
|
+
// Value message: F2 = 0 (simple varint, matching real Resolve format)
|
|
1359
|
+
// Real Resolve uses: 10 00 = F2 = 0
|
|
1360
|
+
const valueMsg = protoVarint(2, 0);
|
|
1361
|
+
|
|
1362
|
+
// Outer entry: F1=presenceId, F2=valueMsg
|
|
1363
|
+
const entry = Buffer.concat([
|
|
1364
|
+
protoVarint(1, presenceId),
|
|
1365
|
+
protoBytes(2, valueMsg),
|
|
1366
|
+
]);
|
|
1367
|
+
|
|
1368
|
+
return protoBytes(3, entry);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* Build a presence marker corrector block (for types 2, 3, 4, 5, 6, 18)
|
|
1373
|
+
* These are minimal correctors that tell Resolve the node has these capabilities
|
|
1374
|
+
*/
|
|
1375
|
+
function buildPresenceMarkerCorrectorBlock(correctorType) {
|
|
1376
|
+
const presenceEntry = createPresenceMarkerEntry(correctorType);
|
|
1377
|
+
if (!presenceEntry) return Buffer.alloc(0);
|
|
1378
|
+
|
|
1379
|
+
// Parameter container: F1=1 (header), F3=presence entry
|
|
1380
|
+
const field2Content = Buffer.concat([
|
|
1381
|
+
protoVarint(1, 1),
|
|
1382
|
+
presenceEntry,
|
|
1383
|
+
]);
|
|
1384
|
+
const field2 = protoBytes(2, field2Content);
|
|
1385
|
+
|
|
1386
|
+
// Outer container
|
|
1387
|
+
const field6 = protoBytes(6, field2);
|
|
1388
|
+
|
|
1389
|
+
// Build corrector block
|
|
1390
|
+
const correctorBlock = Buffer.concat([
|
|
1391
|
+
protoVarint(1, correctorType),
|
|
1392
|
+
protoVarint(3, 1), // Enabled
|
|
1393
|
+
field6,
|
|
1394
|
+
]);
|
|
1395
|
+
|
|
1396
|
+
return protoBytes(1, correctorBlock);
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/**
|
|
1400
|
+
* Build a corrector block (wraps parameters in proper protobuf structure)
|
|
1401
|
+
*
|
|
1402
|
+
* Based on training-analyzer.js parser, the structure is:
|
|
1403
|
+
* Field 1 (corrector block wrapper):
|
|
1404
|
+
* Field 1: corrector type ID
|
|
1405
|
+
* Field 3: enabled flag (1 = enabled)
|
|
1406
|
+
* Field 6: outer container
|
|
1407
|
+
* Field 2: parameter container
|
|
1408
|
+
* Field 1: header (value 1)
|
|
1409
|
+
* Field 3[]: parameter entries (Field 1=ID, Field 2={Field 1=float})
|
|
1410
|
+
*/
|
|
1411
|
+
function buildCorrectorBlock(correctorType, paramEntries) {
|
|
1412
|
+
if (paramEntries.length === 0) return Buffer.alloc(0);
|
|
1413
|
+
|
|
1414
|
+
// Parameter container: Field 1 = 1 (header), then Field 3[] for each param
|
|
1415
|
+
const field2Content = Buffer.concat([
|
|
1416
|
+
protoVarint(1, 1), // Required header field (F1 = 1)
|
|
1417
|
+
...paramEntries, // Each param is wrapped in Field 3
|
|
1418
|
+
]);
|
|
1419
|
+
const field2 = protoBytes(2, field2Content);
|
|
1420
|
+
|
|
1421
|
+
// Outer container: Field 6 wraps Field 2
|
|
1422
|
+
const field6 = protoBytes(6, field2);
|
|
1423
|
+
|
|
1424
|
+
// Build corrector block
|
|
1425
|
+
const correctorBlock = Buffer.concat([
|
|
1426
|
+
protoVarint(1, correctorType), // Corrector type ID
|
|
1427
|
+
protoVarint(3, 1), // Enabled flag
|
|
1428
|
+
field6, // Parameters in F6->F2
|
|
1429
|
+
]);
|
|
1430
|
+
|
|
1431
|
+
debugLog('[DRX] Built corrector block type:', correctorType, 'with', paramEntries.length, 'params');
|
|
1432
|
+
return protoBytes(1, correctorBlock);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Create a node connection
|
|
1437
|
+
*/
|
|
1438
|
+
function createConnection(sourceNode, targetNode, index) {
|
|
1439
|
+
const conn = Buffer.concat([
|
|
1440
|
+
protoVarint(1, sourceNode),
|
|
1441
|
+
protoVarint(3, targetNode),
|
|
1442
|
+
protoVarint(5, 64),
|
|
1443
|
+
protoVarint(6, 64),
|
|
1444
|
+
protoVarint(7, index),
|
|
1445
|
+
]);
|
|
1446
|
+
return protoBytes(8, conn);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Create resolution/transform message
|
|
1451
|
+
*/
|
|
1452
|
+
function createResolution(width = 1920, height = 1080) {
|
|
1453
|
+
return protoBytes(3, Buffer.concat([
|
|
1454
|
+
protoVarint(1, width),
|
|
1455
|
+
protoVarint(2, height),
|
|
1456
|
+
protoFloat32(3, 1.0),
|
|
1457
|
+
protoVarint(4, width),
|
|
1458
|
+
protoVarint(5, height),
|
|
1459
|
+
protoFloat32(6, 1.0),
|
|
1460
|
+
protoVarint(7, width),
|
|
1461
|
+
protoVarint(8, height),
|
|
1462
|
+
protoVarint(9, 0xFFFFFFFF), // F9 = -1 (required by Resolve, matches native exports)
|
|
1463
|
+
]));
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/**
|
|
1467
|
+
* Create Input/Output node markers (Field 9 and Field 10 in main container)
|
|
1468
|
+
* These are required for Resolve to recognize the grade structure
|
|
1469
|
+
*
|
|
1470
|
+
* Based on ULTRA-DEEP analysis of Resolve's 1-6 node DRX exports:
|
|
1471
|
+
* Pattern discovered (using firstNodeId=36 as example):
|
|
1472
|
+
* - Input (F9): F1=66(+30), F2=80, F3={F1=69(+33), F2=64, F3=66(+30), F4=firstNodeId}
|
|
1473
|
+
* - Output (F10): F1=67(+31), F2=64, F3={F1=69+nodeCount, F2=64, F3=67(+31), F4=lastNodeId}
|
|
1474
|
+
*
|
|
1475
|
+
* The input marker values are CONSTANT (don't change with node count)
|
|
1476
|
+
* Only output F3.F1 and F3.F4 change based on node count
|
|
1477
|
+
*
|
|
1478
|
+
* @param {number} firstNodeId - ID of the first node (input connects TO this)
|
|
1479
|
+
* @param {number} lastNodeId - ID of the last node (output connects FROM this)
|
|
1480
|
+
* @param {number} nodeCount - Total number of nodes
|
|
1481
|
+
*/
|
|
1482
|
+
function createInputOutputMarkers(firstNodeId = 1, lastNodeId = 1, nodeCount = 1) {
|
|
1483
|
+
// CRITICAL: F9/F10 values must be calculated from actual node IDs
|
|
1484
|
+
//
|
|
1485
|
+
// Analysis of Resolve's native DRX export for node ID 66:
|
|
1486
|
+
// F9.F1 = 72 = 66 + 6
|
|
1487
|
+
// F10.F1 = 73 = 66 + 7
|
|
1488
|
+
// F9.F3.F1 = 88 = 66 + 22
|
|
1489
|
+
// F10.F3.F1 = 89 = 66 + 23
|
|
1490
|
+
//
|
|
1491
|
+
// The formula is:
|
|
1492
|
+
// F9.F1 = firstNodeId + 6
|
|
1493
|
+
// F10.F1 = lastNodeId + 7
|
|
1494
|
+
// F9.F3.F1 = firstNodeId + 22
|
|
1495
|
+
// F10.F3.F1 = lastNodeId + 23
|
|
1496
|
+
//
|
|
1497
|
+
// Using actual node IDs ensures Resolve recognizes the grade structure
|
|
1498
|
+
|
|
1499
|
+
const inputF1 = firstNodeId + 6; // F9.F1 = firstNodeId + 6
|
|
1500
|
+
const outputF1 = lastNodeId + 7; // F10.F1 = lastNodeId + 7
|
|
1501
|
+
const inputInnerF1 = firstNodeId + 22; // F9.F3.F1 = firstNodeId + 22
|
|
1502
|
+
const outputInnerF1 = lastNodeId + 23; // F10.F3.F1 = lastNodeId + 23
|
|
1503
|
+
|
|
1504
|
+
// Field 9: Input node marker
|
|
1505
|
+
// Structure: F1=inputF1, F2=80, F3={F1=inputInnerF1, F2=64, F3=inputF1, F4=firstNodeId}
|
|
1506
|
+
const inputInner = Buffer.concat([
|
|
1507
|
+
protoVarint(1, inputInnerF1), // F1 = firstNodeId + 22
|
|
1508
|
+
protoVarint(2, 64), // F2 = 64
|
|
1509
|
+
protoVarint(3, inputF1), // F3 = same as outer F1
|
|
1510
|
+
protoVarint(4, firstNodeId), // F4 = first node ID (chainStart)
|
|
1511
|
+
]);
|
|
1512
|
+
const inputContent = Buffer.concat([
|
|
1513
|
+
protoVarint(1, inputF1), // F1 = firstNodeId + 6
|
|
1514
|
+
protoVarint(2, 80), // F2 = 80 (0x50)
|
|
1515
|
+
protoBytes(3, inputInner), // F3 = connection info
|
|
1516
|
+
]);
|
|
1517
|
+
const inputMarker = protoBytes(9, inputContent);
|
|
1518
|
+
|
|
1519
|
+
// Field 10: Output node marker
|
|
1520
|
+
// Structure: F1=outputF1, F2=64, F3={F1=outputInnerF1, F2=64, F3=outputF1, F4=lastNodeId}
|
|
1521
|
+
const outputInner = Buffer.concat([
|
|
1522
|
+
protoVarint(1, outputInnerF1), // F1 = lastNodeId + 23
|
|
1523
|
+
protoVarint(2, 64), // F2 = 64
|
|
1524
|
+
protoVarint(3, outputF1), // F3 = same as outer F1
|
|
1525
|
+
protoVarint(4, lastNodeId), // F4 = last node ID (chainEnd)
|
|
1526
|
+
]);
|
|
1527
|
+
const outputContent = Buffer.concat([
|
|
1528
|
+
protoVarint(1, outputF1), // F1 = lastNodeId + 7
|
|
1529
|
+
protoVarint(2, 64), // F2 = 64 (0x40)
|
|
1530
|
+
protoBytes(3, outputInner), // F3 = connection info
|
|
1531
|
+
]);
|
|
1532
|
+
const outputMarker = protoBytes(10, outputContent);
|
|
1533
|
+
|
|
1534
|
+
return { inputMarker, outputMarker };
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
/**
|
|
1538
|
+
* Generate a timestamp for DRX files (used in Field 12 and top-level Field 4)
|
|
1539
|
+
* Based on analysis, this appears to be a time-based value
|
|
1540
|
+
*/
|
|
1541
|
+
function generateTimestamp() {
|
|
1542
|
+
// Use current time in a format similar to what Resolve uses
|
|
1543
|
+
// The sample value was 149252784 which seems to be seconds from some epoch
|
|
1544
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1545
|
+
return now;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Generate the static pTrackVer body
|
|
1550
|
+
* From analysis: pTrackVer is IDENTICAL across 1-6 node DRX files
|
|
1551
|
+
* Only F1.F12 (timestamp) changes
|
|
1552
|
+
*
|
|
1553
|
+
* Structure:
|
|
1554
|
+
* - F1.F1 = 0xFFFFFFFF (no version)
|
|
1555
|
+
* - F1.F2 = 1
|
|
1556
|
+
* - F1.F3 = resolution (1920x1080)
|
|
1557
|
+
* - F1.F4 = 310722
|
|
1558
|
+
* - F1.F9 = {F1=2, F2=64} (input marker)
|
|
1559
|
+
* - F1.F10 = {F1=3, F2=64} (output marker)
|
|
1560
|
+
* - F1.F11 = 1
|
|
1561
|
+
* - F1.F12 = timestamp
|
|
1562
|
+
* - F3 = {F4=190, F5=180, F7=1, F8=85} (node position info)
|
|
1563
|
+
*/
|
|
1564
|
+
async function generatePTrackVerBody(width = 1920, height = 1080) {
|
|
1565
|
+
const timestamp = generateTimestamp();
|
|
1566
|
+
|
|
1567
|
+
// F1 container
|
|
1568
|
+
const f1Parts = [
|
|
1569
|
+
protoVarint(1, 0xFFFFFFFF), // F1.F1 = 0xFFFFFFFF (no version)
|
|
1570
|
+
protoVarint(2, 1), // F1.F2 = 1
|
|
1571
|
+
createResolution(width, height), // F1.F3 = resolution
|
|
1572
|
+
protoVarint(4, 310722), // F1.F4 = 310722 (constant)
|
|
1573
|
+
];
|
|
1574
|
+
|
|
1575
|
+
// F1.F9 = input marker (simplified)
|
|
1576
|
+
const f1f9 = protoBytes(9, Buffer.concat([
|
|
1577
|
+
protoVarint(1, 2),
|
|
1578
|
+
protoVarint(2, 64),
|
|
1579
|
+
]));
|
|
1580
|
+
f1Parts.push(f1f9);
|
|
1581
|
+
|
|
1582
|
+
// F1.F10 = output marker (simplified)
|
|
1583
|
+
const f1f10 = protoBytes(10, Buffer.concat([
|
|
1584
|
+
protoVarint(1, 3),
|
|
1585
|
+
protoVarint(2, 64),
|
|
1586
|
+
]));
|
|
1587
|
+
f1Parts.push(f1f10);
|
|
1588
|
+
|
|
1589
|
+
f1Parts.push(protoVarint(11, 1)); // F1.F11 = 1
|
|
1590
|
+
f1Parts.push(protoVarint(12, timestamp)); // F1.F12 = timestamp
|
|
1591
|
+
|
|
1592
|
+
const f1Content = protoBytes(1, Buffer.concat(f1Parts));
|
|
1593
|
+
|
|
1594
|
+
// F3 = node position info (matches native Resolve export)
|
|
1595
|
+
const f3Parts = [
|
|
1596
|
+
protoVarint(4, 190), // F3.F4 = 190 (x pos)
|
|
1597
|
+
protoVarint(5, 180), // F3.F5 = 180 (y pos)
|
|
1598
|
+
protoVarint(7, 1), // F3.F7 = 1
|
|
1599
|
+
protoVarint(8, 85), // F3.F8 = 85 (0x55, matches native Resolve export)
|
|
1600
|
+
protoBytes(9, Buffer.alloc(0)), // F3.F9 = empty
|
|
1601
|
+
];
|
|
1602
|
+
|
|
1603
|
+
// F3.F10 = node metadata
|
|
1604
|
+
const f3f10Inner = protoBytes(1, Buffer.concat([
|
|
1605
|
+
protoVarint(1, 0xC0000001), // F1 = 0xC0000001
|
|
1606
|
+
protoBytes(2, protoVarint(2, 2)), // F2 = {F2=2}
|
|
1607
|
+
]));
|
|
1608
|
+
f3Parts.push(protoBytes(10, f3f10Inner));
|
|
1609
|
+
f3Parts.push(protoVarint(12, timestamp + 100)); // F3.F12 = timestamp + offset
|
|
1610
|
+
|
|
1611
|
+
const f3Content = protoBytes(3, Buffer.concat(f3Parts));
|
|
1612
|
+
|
|
1613
|
+
// Combine F1 and F3
|
|
1614
|
+
const trackBody = Buffer.concat([f1Content, f3Content]);
|
|
1615
|
+
|
|
1616
|
+
// Compress
|
|
1617
|
+
const zstd = await getZstdCompressor();
|
|
1618
|
+
const compressed = zstd.compress(trackBody);
|
|
1619
|
+
return Buffer.concat([Buffer.from([0x81]), Buffer.from(compressed)]).toString('hex');
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
/**
|
|
1623
|
+
* Generate the complete protobuf body for a DRX grade
|
|
1624
|
+
*
|
|
1625
|
+
* @param {Object} gradeParams - Color grading parameters (single node) OR array of nodes
|
|
1626
|
+
* @param {Object} options - Generation options
|
|
1627
|
+
* @returns {Buffer} - Uncompressed protobuf data
|
|
1628
|
+
*/
|
|
1629
|
+
function generateProtobuf(gradeParams, options = {}) {
|
|
1630
|
+
const {
|
|
1631
|
+
width = 1920,
|
|
1632
|
+
height = 1080,
|
|
1633
|
+
label = '',
|
|
1634
|
+
} = options;
|
|
1635
|
+
|
|
1636
|
+
const timestamp = generateTimestamp();
|
|
1637
|
+
|
|
1638
|
+
// Build main content for Field 1 container
|
|
1639
|
+
const containerParts = [
|
|
1640
|
+
protoVarint(1, 16), // Version (always 16)
|
|
1641
|
+
protoVarint(2, 1), // Flag (always 1)
|
|
1642
|
+
createResolution(width, height),
|
|
1643
|
+
];
|
|
1644
|
+
|
|
1645
|
+
// Determine first and last node IDs for input/output markers
|
|
1646
|
+
let firstNodeId = 1;
|
|
1647
|
+
let lastNodeId = 1;
|
|
1648
|
+
|
|
1649
|
+
// Check if we have multiple nodes or a single grade
|
|
1650
|
+
if (Array.isArray(gradeParams)) {
|
|
1651
|
+
// Multi-node mode
|
|
1652
|
+
const nodeSpacing = 270;
|
|
1653
|
+
const baseX = 190;
|
|
1654
|
+
const baseY = 180;
|
|
1655
|
+
|
|
1656
|
+
lastNodeId = gradeParams.length; // Last node is the count of nodes
|
|
1657
|
+
|
|
1658
|
+
gradeParams.forEach((nodeConfig, index) => {
|
|
1659
|
+
const nodeId = index + 1;
|
|
1660
|
+
const xPos = baseX + (index * nodeSpacing);
|
|
1661
|
+
const yPos = nodeConfig.yPos || baseY;
|
|
1662
|
+
const node = createNode(nodeId, xPos, yPos, nodeConfig.params, {
|
|
1663
|
+
label: nodeConfig.label || `Node ${nodeId}`,
|
|
1664
|
+
enabled: nodeConfig.enabled !== false,
|
|
1665
|
+
});
|
|
1666
|
+
containerParts.push(node);
|
|
1667
|
+
});
|
|
1668
|
+
|
|
1669
|
+
// Create serial connections between nodes
|
|
1670
|
+
for (let i = 1; i < gradeParams.length; i++) {
|
|
1671
|
+
const conn = createConnection(i, i + 1, i);
|
|
1672
|
+
containerParts.push(conn);
|
|
1673
|
+
}
|
|
1674
|
+
} else {
|
|
1675
|
+
// Single node mode (backwards compatible)
|
|
1676
|
+
// SAFETY: if gradeParams hasn't been through parseAdjustments yet (e.g. has
|
|
1677
|
+
// a 'mode' key or semantic-range temperature like 0.5), warn — callers should
|
|
1678
|
+
// always run parseAdjustments() first so scaling and clamping are applied.
|
|
1679
|
+
if (gradeParams.temperature !== undefined && Math.abs(gradeParams.temperature) <= 1 && gradeParams.temperature !== 0) {
|
|
1680
|
+
console.warn('[DRX] WARNING: gradeParams.temperature=' + gradeParams.temperature + ' looks like semantic range (-1 to +1) — did you forget to call parseAdjustments() first?');
|
|
1681
|
+
}
|
|
1682
|
+
const node = createNode(1, 500, 300, gradeParams, {
|
|
1683
|
+
label: label,
|
|
1684
|
+
});
|
|
1685
|
+
containerParts.push(node);
|
|
1686
|
+
// firstNodeId and lastNodeId both stay 1
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// Determine node count for markers
|
|
1690
|
+
const nodeCount = Array.isArray(gradeParams) ? gradeParams.length : 1;
|
|
1691
|
+
|
|
1692
|
+
// Create input/output markers with correct node IDs and count
|
|
1693
|
+
const { inputMarker, outputMarker } = createInputOutputMarkers(firstNodeId, lastNodeId, nodeCount);
|
|
1694
|
+
|
|
1695
|
+
// Add required input/output markers and metadata
|
|
1696
|
+
containerParts.push(inputMarker);
|
|
1697
|
+
containerParts.push(outputMarker);
|
|
1698
|
+
containerParts.push(protoVarint(11, 1)); // Unknown flag (always 1)
|
|
1699
|
+
containerParts.push(protoVarint(12, timestamp)); // Timestamp
|
|
1700
|
+
|
|
1701
|
+
// Wrap in root Field 1
|
|
1702
|
+
const mainContent = protoBytes(1, Buffer.concat(containerParts));
|
|
1703
|
+
|
|
1704
|
+
// Add root-level F3 (node position info) - matches native format
|
|
1705
|
+
// Structure: F3 = {F4=190, F5=180, F7=1, F8=84, F9=empty, F10={F1={F1=0xC0000001, F2={F2=2}}}, F12=timestamp}
|
|
1706
|
+
const rootF3InnerInner = Buffer.concat([
|
|
1707
|
+
protoVarint(1, 0xC0000001), // F1 = 0xC0000001
|
|
1708
|
+
protoBytes(2, protoVarint(2, 2)), // F2 = {F2=2}
|
|
1709
|
+
]);
|
|
1710
|
+
const rootF3F10 = protoBytes(1, rootF3InnerInner);
|
|
1711
|
+
|
|
1712
|
+
const rootF3Parts = [
|
|
1713
|
+
protoVarint(4, 190), // F4 = 190 (x pos)
|
|
1714
|
+
protoVarint(5, 180), // F5 = 180 (y pos)
|
|
1715
|
+
protoVarint(7, 1), // F7 = 1
|
|
1716
|
+
protoVarint(8, 84), // F8 = 84 (native analysis shows 0x54)
|
|
1717
|
+
protoBytes(9, Buffer.alloc(0)), // F9 = empty
|
|
1718
|
+
protoBytes(10, rootF3F10), // F10 = node metadata
|
|
1719
|
+
protoVarint(12, timestamp), // F12 = timestamp
|
|
1720
|
+
];
|
|
1721
|
+
const rootF3 = protoBytes(3, Buffer.concat(rootF3Parts));
|
|
1722
|
+
|
|
1723
|
+
// Combine F1 and F3 at root level
|
|
1724
|
+
const topLevel = Buffer.concat([mainContent, rootF3]);
|
|
1725
|
+
|
|
1726
|
+
return topLevel;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/**
|
|
1730
|
+
* Generate a multi-node DRX with custom node graph
|
|
1731
|
+
*
|
|
1732
|
+
* @param {Array} nodes - Array of node configurations
|
|
1733
|
+
* @param {Array} connections - Array of connection objects {from, to}
|
|
1734
|
+
* @param {Object} metadata - DRX metadata
|
|
1735
|
+
* @returns {Promise<string>} - Complete DRX XML content
|
|
1736
|
+
*/
|
|
1737
|
+
async function generateMultiNodeDRX(nodes, connections, metadata = {}) {
|
|
1738
|
+
const {
|
|
1739
|
+
label = 'AI Multi-Node Grade',
|
|
1740
|
+
width = 1920,
|
|
1741
|
+
height = 1080,
|
|
1742
|
+
sourceTimeline = 'Timeline 1',
|
|
1743
|
+
sourceTC = '00:00:00:00',
|
|
1744
|
+
recordTC = '01:00:00:00',
|
|
1745
|
+
pTrackVerXml = null, // Preserved from original DRX during merge
|
|
1746
|
+
preserveNodeIds = false, // When true, use node.id as-is (for merge mode)
|
|
1747
|
+
baseNodeId: passedBaseNodeId,
|
|
1748
|
+
firstNodeId: passedFirstNodeId,
|
|
1749
|
+
lastNodeId: passedLastNodeId,
|
|
1750
|
+
} = metadata;
|
|
1751
|
+
|
|
1752
|
+
const timestamp = generateTimestamp();
|
|
1753
|
+
|
|
1754
|
+
// Determine node ID scheme
|
|
1755
|
+
let baseNodeId, firstNodeId, lastNodeId;
|
|
1756
|
+
if (preserveNodeIds && passedBaseNodeId !== undefined) {
|
|
1757
|
+
// MERGE MODE: use preserved IDs from original grade
|
|
1758
|
+
baseNodeId = passedBaseNodeId;
|
|
1759
|
+
firstNodeId = passedFirstNodeId;
|
|
1760
|
+
lastNodeId = passedLastNodeId;
|
|
1761
|
+
debugLog('[DRX] generateMultiNodeDRX: MERGE MODE - preserving node IDs', firstNodeId, '-', lastNodeId);
|
|
1762
|
+
} else {
|
|
1763
|
+
// FRESH MODE: use standard ID scheme starting from 2 (matches production DRX)
|
|
1764
|
+
// Production DRX files NEVER use node ID 1 - minimum is always 2
|
|
1765
|
+
// Analysis of 240 production files shows minNodeId is always >= 2
|
|
1766
|
+
baseNodeId = 1; // So first node is 2 (production pattern)
|
|
1767
|
+
firstNodeId = baseNodeId + 1;
|
|
1768
|
+
lastNodeId = baseNodeId + nodes.length;
|
|
1769
|
+
debugLog('[DRX] generateMultiNodeDRX: FRESH MODE - generating IDs', firstNodeId, '-', lastNodeId);
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
const version = lastNodeId; // Resolve sets version = lastNodeId
|
|
1773
|
+
const nodeCount = nodes.length;
|
|
1774
|
+
|
|
1775
|
+
// Build main content for Field 1 container
|
|
1776
|
+
const containerParts = [
|
|
1777
|
+
protoVarint(1, version), // Version = lastNodeId (Resolve pattern)
|
|
1778
|
+
protoVarint(2, 1), // Flag
|
|
1779
|
+
createResolution(width, height),
|
|
1780
|
+
];
|
|
1781
|
+
|
|
1782
|
+
// Add all nodes
|
|
1783
|
+
debugLog('[DRX] generateMultiNodeDRX: processing', nodes.length, 'nodes');
|
|
1784
|
+
nodes.forEach((nodeConfig, index) => {
|
|
1785
|
+
// Use node's own ID if preserving, otherwise compute from baseNodeId
|
|
1786
|
+
const nodeId = preserveNodeIds ? nodeConfig.id : (baseNodeId + index + 1);
|
|
1787
|
+
const nodeIndex = index + 1; // 1, 2, 3, ... (F2 field)
|
|
1788
|
+
debugLog(`[DRX] Node ${nodeIndex}: id=${nodeId}, label="${nodeConfig.label}", params:`, nodeConfig.params ? 'present' : 'null');
|
|
1789
|
+
if (nodeConfig.params) {
|
|
1790
|
+
debugLog(`[DRX] Node ${nodeIndex} params keys:`, Object.keys(nodeConfig.params));
|
|
1791
|
+
}
|
|
1792
|
+
const node = createNode(
|
|
1793
|
+
nodeId,
|
|
1794
|
+
nodeConfig.xPos || 190 + (index * 270),
|
|
1795
|
+
nodeConfig.yPos || 180,
|
|
1796
|
+
nodeConfig.params,
|
|
1797
|
+
{
|
|
1798
|
+
label: nodeConfig.label || `Node ${nodeIndex}`,
|
|
1799
|
+
enabled: nodeConfig.enabled !== false,
|
|
1800
|
+
nodeIndex: nodeIndex, // Pass separate index for F2 field
|
|
1801
|
+
}
|
|
1802
|
+
);
|
|
1803
|
+
containerParts.push(node);
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
// Add all connections using Resolve's ID scheme
|
|
1807
|
+
// Connection F7 starts at (firstNodeId - 40) and increments (discovered from Resolve export analysis)
|
|
1808
|
+
// Example: 10-node export with firstNodeId=70 uses F7 = 30, 31, 32...
|
|
1809
|
+
const connBaseF7 = preserveNodeIds ? Math.max(firstNodeId - 40, 3) : 3;
|
|
1810
|
+
connections.forEach((conn, index) => {
|
|
1811
|
+
// In merge mode, connections already have actual node IDs
|
|
1812
|
+
// In fresh mode, connections are 1-based and need baseNodeId offset
|
|
1813
|
+
const fromId = preserveNodeIds ? conn.from : (baseNodeId + conn.from);
|
|
1814
|
+
const toId = preserveNodeIds ? conn.to : (baseNodeId + conn.to);
|
|
1815
|
+
const connIndex = connBaseF7 + index;
|
|
1816
|
+
const connection = createConnection(fromId, toId, connIndex);
|
|
1817
|
+
containerParts.push(connection);
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1820
|
+
// Create input/output markers with correct node IDs and node count
|
|
1821
|
+
const { inputMarker, outputMarker } = createInputOutputMarkers(firstNodeId, lastNodeId, nodeCount);
|
|
1822
|
+
|
|
1823
|
+
// Add required input/output markers and metadata
|
|
1824
|
+
containerParts.push(inputMarker);
|
|
1825
|
+
containerParts.push(outputMarker);
|
|
1826
|
+
containerParts.push(protoVarint(11, 1)); // Unknown flag (always 1)
|
|
1827
|
+
containerParts.push(protoVarint(12, timestamp)); // Timestamp
|
|
1828
|
+
|
|
1829
|
+
// Wrap in root Field 1
|
|
1830
|
+
const mainContent = protoBytes(1, Buffer.concat(containerParts));
|
|
1831
|
+
|
|
1832
|
+
// Add root-level F3 (node position info) - native Resolve includes this
|
|
1833
|
+
// Structure from native analysis:
|
|
1834
|
+
// F3 = {F4=190, F5=180, F7=1, F8=84, F9=empty, F10={F1={F1=0xC0000001, F2={F2=2}}}, F12=timestamp}
|
|
1835
|
+
const rootF3InnerInner = Buffer.concat([
|
|
1836
|
+
protoVarint(1, 0xC0000001), // F1 = 0xC0000001
|
|
1837
|
+
protoBytes(2, protoVarint(2, 2)), // F2 = {F2=2}
|
|
1838
|
+
]);
|
|
1839
|
+
const rootF3F10 = protoBytes(1, rootF3InnerInner);
|
|
1840
|
+
|
|
1841
|
+
const rootF3Parts = [
|
|
1842
|
+
protoVarint(4, 190), // F4 = 190 (x pos)
|
|
1843
|
+
protoVarint(5, 180), // F5 = 180 (y pos)
|
|
1844
|
+
protoVarint(7, 1), // F7 = 1
|
|
1845
|
+
protoVarint(8, 84), // F8 = 84 (NOT 85 - native analysis shows 0x54 = 84)
|
|
1846
|
+
protoBytes(9, Buffer.alloc(0)), // F9 = empty (native has empty F9)
|
|
1847
|
+
protoBytes(10, rootF3F10), // F10 = node metadata
|
|
1848
|
+
protoVarint(12, timestamp), // F12 = timestamp
|
|
1849
|
+
];
|
|
1850
|
+
const rootF3 = protoBytes(3, Buffer.concat(rootF3Parts));
|
|
1851
|
+
|
|
1852
|
+
// Combine F1 and F3 at root level
|
|
1853
|
+
const topLevel = Buffer.concat([mainContent, rootF3]);
|
|
1854
|
+
|
|
1855
|
+
// Compress and generate DRX
|
|
1856
|
+
const zstd = await getZstdCompressor();
|
|
1857
|
+
const compressed = zstd.compress(topLevel);
|
|
1858
|
+
const body = Buffer.concat([Buffer.from([0x81]), Buffer.from(compressed)]);
|
|
1859
|
+
const bodyHex = body.toString('hex');
|
|
1860
|
+
|
|
1861
|
+
// Generate UUIDs
|
|
1862
|
+
const { v4: uuidv4 } = require('uuid');
|
|
1863
|
+
const stillId = uuidv4();
|
|
1864
|
+
const clipVersionId = uuidv4();
|
|
1865
|
+
|
|
1866
|
+
const now = new Date().toISOString().replace('Z', '');
|
|
1867
|
+
|
|
1868
|
+
// Build pTrackVer section - always include it (native Resolve DRX always has pTrackVer)
|
|
1869
|
+
// Use preserved one if available, otherwise generate a default one
|
|
1870
|
+
let pTrackVerSection;
|
|
1871
|
+
if (pTrackVerXml) {
|
|
1872
|
+
pTrackVerSection = `
|
|
1873
|
+
${pTrackVerXml}`;
|
|
1874
|
+
} else {
|
|
1875
|
+
// Generate default pTrackVer body
|
|
1876
|
+
const trackVersionId = require('uuid').v4();
|
|
1877
|
+
const trackBodyHex = await generatePTrackVerBody(width, height);
|
|
1878
|
+
pTrackVerSection = `
|
|
1879
|
+
<pTrackVer>
|
|
1880
|
+
<ListMgt::LmVersion DbId="${trackVersionId}">
|
|
1881
|
+
<FieldsBlob/>
|
|
1882
|
+
<Name/>
|
|
1883
|
+
<HasCorrection>false</HasCorrection>
|
|
1884
|
+
<VerType>1</VerType>
|
|
1885
|
+
<ImplVersion>1</ImplVersion>
|
|
1886
|
+
<IncludedInRecording>true</IncludedInRecording>
|
|
1887
|
+
<FlatPassEnabled>false</FlatPassEnabled>
|
|
1888
|
+
<RGBAOutputEnabled>false</RGBAOutputEnabled>
|
|
1889
|
+
<Body>${trackBodyHex}</Body>
|
|
1890
|
+
<UseVersionClipProcParams>true</UseVersionClipProcParams>
|
|
1891
|
+
</ListMgt::LmVersion>
|
|
1892
|
+
</pTrackVer>`;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1896
|
+
<!--DbAppVer="19.1.3.0007" DbPrjVer="14"-->
|
|
1897
|
+
<Gallery::GyStill DbId="${stillId}">
|
|
1898
|
+
<FieldsBlob/>
|
|
1899
|
+
<SrcHint>${sourceTimeline}</SrcHint>
|
|
1900
|
+
<SrcType>1</SrcType>
|
|
1901
|
+
<GalleryPath/>
|
|
1902
|
+
<Label>${label}</Label>
|
|
1903
|
+
<RecTC>${recordTC}</RecTC>
|
|
1904
|
+
<SrcTC>${sourceTC}</SrcTC>
|
|
1905
|
+
<DpxDescriptor>50</DpxDescriptor>
|
|
1906
|
+
<Width>${width}</Width>
|
|
1907
|
+
<Height>${height}</Height>
|
|
1908
|
+
<BitDepth>10</BitDepth>
|
|
1909
|
+
<PAR>1</PAR>
|
|
1910
|
+
<Endianship>1</Endianship>
|
|
1911
|
+
<CreateTime>${now}</CreateTime>
|
|
1912
|
+
<pClipFullVer>
|
|
1913
|
+
<ListMgt::LmVersion DbId="${clipVersionId}">
|
|
1914
|
+
<FieldsBlob/>
|
|
1915
|
+
<Name/>
|
|
1916
|
+
<HasCorrection>true</HasCorrection>
|
|
1917
|
+
<VerType>0</VerType>
|
|
1918
|
+
<ImplVersion>1</ImplVersion>
|
|
1919
|
+
<IncludedInRecording>true</IncludedInRecording>
|
|
1920
|
+
<FlatPassEnabled>false</FlatPassEnabled>
|
|
1921
|
+
<RGBAOutputEnabled>false</RGBAOutputEnabled>
|
|
1922
|
+
<Body>${bodyHex}</Body>
|
|
1923
|
+
<UseVersionClipProcParams>true</UseVersionClipProcParams>
|
|
1924
|
+
</ListMgt::LmVersion>
|
|
1925
|
+
</pClipFullVer>${pTrackVerSection}
|
|
1926
|
+
<PrimaryCCMode>0</PrimaryCCMode>
|
|
1927
|
+
</Gallery::GyStill>
|
|
1928
|
+
`;
|
|
1929
|
+
|
|
1930
|
+
return xml;
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
/**
|
|
1934
|
+
* Generate a DRX Body (compressed format)
|
|
1935
|
+
*
|
|
1936
|
+
* @param {Object} gradeParams - Color grading parameters
|
|
1937
|
+
* @param {Object} options - Generation options
|
|
1938
|
+
* @returns {Promise<Buffer>} - Complete DRX body ([0x81] + ZSTD compressed protobuf)
|
|
1939
|
+
*/
|
|
1940
|
+
async function generateDRXBody(gradeParams, options = {}) {
|
|
1941
|
+
// Generate protobuf
|
|
1942
|
+
const protobufData = generateProtobuf(gradeParams, options);
|
|
1943
|
+
|
|
1944
|
+
// Get ZSTD compressor
|
|
1945
|
+
const zstd = await getZstdCompressor();
|
|
1946
|
+
|
|
1947
|
+
// Compress with ZSTD (returns Uint8Array)
|
|
1948
|
+
const compressed = zstd.compress(protobufData);
|
|
1949
|
+
|
|
1950
|
+
// Prepend type byte
|
|
1951
|
+
return Buffer.concat([Buffer.from([0x81]), Buffer.from(compressed)]);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
/**
|
|
1955
|
+
* Generate complete DRX XML file
|
|
1956
|
+
*
|
|
1957
|
+
* @param {Object} gradeParams - Color grading parameters
|
|
1958
|
+
* @param {Object} metadata - DRX metadata (label, source, etc.)
|
|
1959
|
+
* @returns {Promise<string>} - Complete DRX XML content
|
|
1960
|
+
*/
|
|
1961
|
+
async function generateDRX(gradeParams, metadata = {}) {
|
|
1962
|
+
const {
|
|
1963
|
+
label = 'AI Generated Grade',
|
|
1964
|
+
width = 1920,
|
|
1965
|
+
height = 1080,
|
|
1966
|
+
sourceTimeline = 'Timeline 1',
|
|
1967
|
+
sourceTC = '00:00:00:00',
|
|
1968
|
+
recordTC = '01:00:00:00',
|
|
1969
|
+
} = metadata;
|
|
1970
|
+
|
|
1971
|
+
// Generate the body (pass label for single-node mode)
|
|
1972
|
+
const body = await generateDRXBody(gradeParams, { width, height, label });
|
|
1973
|
+
const bodyHex = body.toString('hex');
|
|
1974
|
+
|
|
1975
|
+
// Generate UUIDs
|
|
1976
|
+
const stillId = uuidv4();
|
|
1977
|
+
const versionId = uuidv4();
|
|
1978
|
+
const thumbnailId = uuidv4();
|
|
1979
|
+
|
|
1980
|
+
// Current timestamp
|
|
1981
|
+
const now = new Date().toISOString().replace('Z', '');
|
|
1982
|
+
|
|
1983
|
+
// Build DRX XML
|
|
1984
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1985
|
+
<!--DbAppVer="19.1.3.0007" DbPrjVer="14"-->
|
|
1986
|
+
<Gallery::GyStill DbId="${stillId}">
|
|
1987
|
+
<FieldsBlob/>
|
|
1988
|
+
<SrcHint>${sourceTimeline}</SrcHint>
|
|
1989
|
+
<SrcType>1</SrcType>
|
|
1990
|
+
<GalleryPath/>
|
|
1991
|
+
<Label>${label}</Label>
|
|
1992
|
+
<RecTC>${recordTC}</RecTC>
|
|
1993
|
+
<SrcTC>${sourceTC}</SrcTC>
|
|
1994
|
+
<DpxDescriptor>50</DpxDescriptor>
|
|
1995
|
+
<Width>${width}</Width>
|
|
1996
|
+
<Height>${height}</Height>
|
|
1997
|
+
<BitDepth>10</BitDepth>
|
|
1998
|
+
<PAR>1</PAR>
|
|
1999
|
+
<Endianship>1</Endianship>
|
|
2000
|
+
<CreateTime>${now}</CreateTime>
|
|
2001
|
+
<pClipFullVer>
|
|
2002
|
+
<ListMgt::LmVersion DbId="${versionId}">
|
|
2003
|
+
<FieldsBlob/>
|
|
2004
|
+
<Name/>
|
|
2005
|
+
<HasCorrection>true</HasCorrection>
|
|
2006
|
+
<VerType>0</VerType>
|
|
2007
|
+
<ImplVersion>1</ImplVersion>
|
|
2008
|
+
<IncludedInRecording>true</IncludedInRecording>
|
|
2009
|
+
<FlatPassEnabled>false</FlatPassEnabled>
|
|
2010
|
+
<RGBAOutputEnabled>false</RGBAOutputEnabled>
|
|
2011
|
+
<Body>${bodyHex}</Body>
|
|
2012
|
+
<UseVersionClipProcParams>true</UseVersionClipProcParams>
|
|
2013
|
+
</ListMgt::LmVersion>
|
|
2014
|
+
</pClipFullVer>
|
|
2015
|
+
<PrimaryCCMode>0</PrimaryCCMode>
|
|
2016
|
+
</Gallery::GyStill>
|
|
2017
|
+
`;
|
|
2018
|
+
|
|
2019
|
+
return xml;
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
/**
|
|
2023
|
+
* Parse cinematographer language into grade parameters
|
|
2024
|
+
* Supports both simple mode (temperature, exposure) and per-channel mode (liftR, gainB)
|
|
2025
|
+
*
|
|
2026
|
+
* @param {Object} adjustments - High-level adjustments from AI
|
|
2027
|
+
* @returns {Object} - Grade parameters for DRX generation
|
|
2028
|
+
*/
|
|
2029
|
+
|
|
2030
|
+
/**
|
|
2031
|
+
* Hard-clamp all grade parameters to valid DRX ranges.
|
|
2032
|
+
* Uses parameter-ranges.js as the single source of truth.
|
|
2033
|
+
* Called at the end of parseAdjustments for both SIMPLE and DIRECT modes
|
|
2034
|
+
* to prevent out-of-range values from reaching the encoder.
|
|
2035
|
+
*
|
|
2036
|
+
* @param {Object} params - Mutable params object (clamped in-place)
|
|
2037
|
+
*/
|
|
2038
|
+
function clampAllParams(params) {
|
|
2039
|
+
// Helper for params without a parameter-ranges entry — uses inline min/max
|
|
2040
|
+
const sc = (val, min, max) => Math.max(min, Math.min(max, val));
|
|
2041
|
+
|
|
2042
|
+
// ── Color Wheels ──────────────────────────────────────────────────────────
|
|
2043
|
+
for (const ch of ['r', 'g', 'b', 'master']) {
|
|
2044
|
+
if (params.lift[ch] !== undefined) params.lift[ch] = drxParams.clamp('lift', ch, params.lift[ch]);
|
|
2045
|
+
if (params.gamma[ch] !== undefined) params.gamma[ch] = drxParams.clamp('gamma', ch, params.gamma[ch]);
|
|
2046
|
+
if (params.gain[ch] !== undefined) params.gain[ch] = drxParams.clamp('gain', ch, params.gain[ch]);
|
|
2047
|
+
}
|
|
2048
|
+
for (const ch of ['r', 'g', 'b']) {
|
|
2049
|
+
if (params.offset[ch] !== undefined) params.offset[ch] = drxParams.clamp('offset', ch, params.offset[ch]);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
// ── Primary Adjustments ───────────────────────────────────────────────────
|
|
2053
|
+
params.saturation = drxParams.clamp('saturation', 'master', params.saturation);
|
|
2054
|
+
params.temperature = drxParams.clamp('temperature', 'master', params.temperature);
|
|
2055
|
+
params.tint = drxParams.clamp('tint', 'master', params.tint);
|
|
2056
|
+
params.contrast = drxParams.clamp('contrast', 'master', params.contrast);
|
|
2057
|
+
params.pivot = drxParams.clamp('pivotFine', 'master', params.pivot);
|
|
2058
|
+
params.midtoneDetail = drxParams.clamp('midtoneDetail', 'master', params.midtoneDetail);
|
|
2059
|
+
|
|
2060
|
+
// ── Color Boost: 0-100 ────────────────────────────────────────────────────
|
|
2061
|
+
if (params.colorBoost !== undefined) {
|
|
2062
|
+
params.colorBoost = drxParams.clamp('colorBoost', 'master', params.colorBoost);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// ── Hue Rotate: 0-100 (UI), default 50 ───────────────────────────────────
|
|
2066
|
+
if (params.hueRotate !== undefined) {
|
|
2067
|
+
params.hueRotate = sc(params.hueRotate, 0, 100);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// ── Highlights/Shadows sliders: -100 to +100 ─────────────────────────────
|
|
2071
|
+
if (params.highlights !== undefined) params.highlights = sc(params.highlights, -100, 100);
|
|
2072
|
+
if (params.shadows !== undefined) params.shadows = sc(params.shadows, -100, 100);
|
|
2073
|
+
|
|
2074
|
+
// ── Soft Clips: 0-1 ──────────────────────────────────────────────────────
|
|
2075
|
+
if (params.softClipHigh !== undefined) params.softClipHigh = drxParams.clamp('softClipHigh', 'master', params.softClipHigh);
|
|
2076
|
+
if (params.softClipLow !== undefined) params.softClipLow = drxParams.clamp('softClipLow', 'master', params.softClipLow);
|
|
2077
|
+
if (params.softClipHighSoft !== undefined) params.softClipHighSoft = sc(params.softClipHighSoft, 0, 100);
|
|
2078
|
+
if (params.softClipLowSoft !== undefined) params.softClipLowSoft = sc(params.softClipLowSoft, 0, 100);
|
|
2079
|
+
|
|
2080
|
+
// ── Contrast Range: 0-1 ──────────────────────────────────────────────────
|
|
2081
|
+
if (params.contrastHighRange !== undefined) params.contrastHighRange = drxParams.clamp('highRange', 'master', params.contrastHighRange);
|
|
2082
|
+
if (params.contrastLowRange !== undefined) params.contrastLowRange = drxParams.clamp('lowRange', 'master', params.contrastLowRange);
|
|
2083
|
+
|
|
2084
|
+
// ── Log Wheels: -1 to +1 per channel ─────────────────────────────────────
|
|
2085
|
+
if (params.logShadow) {
|
|
2086
|
+
for (const ch of ['r', 'g', 'b']) {
|
|
2087
|
+
if (params.logShadow[ch] !== undefined) params.logShadow[ch] = drxParams.clamp('logShadow', ch, params.logShadow[ch]);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
if (params.logMid) {
|
|
2091
|
+
for (const ch of ['r', 'g', 'b']) {
|
|
2092
|
+
if (params.logMid[ch] !== undefined) params.logMid[ch] = drxParams.clamp('logMidtone', ch, params.logMid[ch]);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
if (params.logHigh) {
|
|
2096
|
+
for (const ch of ['r', 'g', 'b']) {
|
|
2097
|
+
if (params.logHigh[ch] !== undefined) params.logHigh[ch] = drxParams.clamp('logHighlight', ch, params.logHigh[ch]);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
// ── HDR Black Offset: -1 to +1 ───────────────────────────────────────────
|
|
2102
|
+
if (params.blackOffset !== undefined) params.blackOffset = drxParams.clamp('hdrBlackOffset', 'master', params.blackOffset);
|
|
2103
|
+
|
|
2104
|
+
// ── RGB Mixer: each channel -2 to +2 (identity diagonal=1.0) ─────────────
|
|
2105
|
+
if (params.rgbMixer) {
|
|
2106
|
+
for (const key of ['rr', 'gr', 'br', 'rg', 'gg', 'bg', 'rb', 'gb', 'bb']) {
|
|
2107
|
+
if (params.rgbMixer[key] !== undefined) params.rgbMixer[key] = sc(params.rgbMixer[key], -2, 2);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
function parseAdjustments(adjustments) {
|
|
2113
|
+
// Deep copy to avoid mutating NEUTRAL_GRADE
|
|
2114
|
+
const params = {
|
|
2115
|
+
lift: { ...NEUTRAL_GRADE.lift },
|
|
2116
|
+
gamma: { ...NEUTRAL_GRADE.gamma },
|
|
2117
|
+
gain: { ...NEUTRAL_GRADE.gain },
|
|
2118
|
+
offset: { ...NEUTRAL_GRADE.offset },
|
|
2119
|
+
saturation: NEUTRAL_GRADE.saturation,
|
|
2120
|
+
contrast: NEUTRAL_GRADE.contrast,
|
|
2121
|
+
pivot: NEUTRAL_GRADE.pivot,
|
|
2122
|
+
temperature: NEUTRAL_GRADE.temperature,
|
|
2123
|
+
tint: NEUTRAL_GRADE.tint,
|
|
2124
|
+
midtoneDetail: NEUTRAL_GRADE.midtoneDetail,
|
|
2125
|
+
softClipHigh: NEUTRAL_GRADE.softClipHigh,
|
|
2126
|
+
softClipLow: NEUTRAL_GRADE.softClipLow,
|
|
2127
|
+
// Log wheels (offset-style grading)
|
|
2128
|
+
logShadow: { r: 0, g: 0, b: 0 },
|
|
2129
|
+
logMid: { r: 0, g: 0, b: 0 },
|
|
2130
|
+
logHigh: { r: 0, g: 0, b: 0 },
|
|
2131
|
+
// Additional controls
|
|
2132
|
+
colorBoost: 0,
|
|
2133
|
+
contrastHighRange: 0.55, // Resolve default ↑ Rng
|
|
2134
|
+
contrastLowRange: 0.333, // Resolve default ↓ Rng
|
|
2135
|
+
// HDR Global controls
|
|
2136
|
+
blackOffset: 0,
|
|
2137
|
+
};
|
|
2138
|
+
|
|
2139
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2140
|
+
// DIRECT MODE — absolute Resolve UI values (calibration-informed)
|
|
2141
|
+
// When mode='direct', values are Resolve-scale: saturation 0-100 (50=unity),
|
|
2142
|
+
// contrast 0-2 (1=unity), lift/gamma/offset -0.5 to +0.5, gain 0.5-2.0, etc.
|
|
2143
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2144
|
+
|
|
2145
|
+
if (adjustments.mode === 'direct') {
|
|
2146
|
+
// Lift (shadows) — R/G/B/Master: -0.5 to +0.5, unity=0
|
|
2147
|
+
if (adjustments.lift) {
|
|
2148
|
+
if (adjustments.lift.r !== undefined) params.lift.r = adjustments.lift.r;
|
|
2149
|
+
if (adjustments.lift.g !== undefined) params.lift.g = adjustments.lift.g;
|
|
2150
|
+
if (adjustments.lift.b !== undefined) params.lift.b = adjustments.lift.b;
|
|
2151
|
+
if (adjustments.lift.master !== undefined) params.lift.master = adjustments.lift.master;
|
|
2152
|
+
}
|
|
2153
|
+
// Gamma (midtones) — R/G/B/Master: -0.5 to +0.5, unity=0
|
|
2154
|
+
if (adjustments.gamma) {
|
|
2155
|
+
if (adjustments.gamma.r !== undefined) params.gamma.r = adjustments.gamma.r;
|
|
2156
|
+
if (adjustments.gamma.g !== undefined) params.gamma.g = adjustments.gamma.g;
|
|
2157
|
+
if (adjustments.gamma.b !== undefined) params.gamma.b = adjustments.gamma.b;
|
|
2158
|
+
if (adjustments.gamma.master !== undefined) params.gamma.master = adjustments.gamma.master;
|
|
2159
|
+
}
|
|
2160
|
+
// Gain (highlights) — R/G/B/Master: 0.5 to 2.0, unity=1.0
|
|
2161
|
+
if (adjustments.gain) {
|
|
2162
|
+
if (adjustments.gain.r !== undefined) params.gain.r = adjustments.gain.r;
|
|
2163
|
+
if (adjustments.gain.g !== undefined) params.gain.g = adjustments.gain.g;
|
|
2164
|
+
if (adjustments.gain.b !== undefined) params.gain.b = adjustments.gain.b;
|
|
2165
|
+
if (adjustments.gain.master !== undefined) params.gain.master = adjustments.gain.master;
|
|
2166
|
+
}
|
|
2167
|
+
// Offset — R/G/B: -0.2 to +0.2, unity=0
|
|
2168
|
+
if (adjustments.offset) {
|
|
2169
|
+
if (adjustments.offset.r !== undefined) params.offset.r = adjustments.offset.r;
|
|
2170
|
+
if (adjustments.offset.g !== undefined) params.offset.g = adjustments.offset.g;
|
|
2171
|
+
if (adjustments.offset.b !== undefined) params.offset.b = adjustments.offset.b;
|
|
2172
|
+
}
|
|
2173
|
+
// Saturation — 0-100, unity=50
|
|
2174
|
+
if (adjustments.saturation !== undefined) params.saturation = adjustments.saturation;
|
|
2175
|
+
// Contrast — 0-2, unity=1.0 (usable range roughly 0.5-1.1 before clipping)
|
|
2176
|
+
if (adjustments.contrast !== undefined) params.contrast = adjustments.contrast;
|
|
2177
|
+
// Pivot — 0-1, default=0.435
|
|
2178
|
+
if (adjustments.pivot !== undefined) params.pivot = adjustments.pivot;
|
|
2179
|
+
// Temperature — -4000 to +4000, unity=0
|
|
2180
|
+
if (adjustments.temperature !== undefined) params.temperature = adjustments.temperature;
|
|
2181
|
+
// Tint — -100 to +100, unity=0
|
|
2182
|
+
if (adjustments.tint !== undefined) params.tint = adjustments.tint;
|
|
2183
|
+
// Midtone Detail — -100 to +100, unity=0
|
|
2184
|
+
if (adjustments.midtoneDetail !== undefined) params.midtoneDetail = adjustments.midtoneDetail;
|
|
2185
|
+
// Log wheels
|
|
2186
|
+
if (adjustments.logShadow) Object.assign(params.logShadow, adjustments.logShadow);
|
|
2187
|
+
if (adjustments.logMid) Object.assign(params.logMid, adjustments.logMid);
|
|
2188
|
+
if (adjustments.logHigh) Object.assign(params.logHigh, adjustments.logHigh);
|
|
2189
|
+
// RGB Mixer
|
|
2190
|
+
if (adjustments.rgbMixer) params.rgbMixer = adjustments.rgbMixer;
|
|
2191
|
+
// Color Boost — 0-100, unity=0
|
|
2192
|
+
if (adjustments.colorBoost !== undefined) params.colorBoost = adjustments.colorBoost;
|
|
2193
|
+
// Hue Rotate — 0-100, unity=50
|
|
2194
|
+
if (adjustments.hueRotate !== undefined) params.hueRotate = adjustments.hueRotate;
|
|
2195
|
+
// Soft Clips — 0-1 (DRX space)
|
|
2196
|
+
if (adjustments.softClipHigh !== undefined) params.softClipHigh = adjustments.softClipHigh;
|
|
2197
|
+
if (adjustments.softClipLow !== undefined) params.softClipLow = adjustments.softClipLow;
|
|
2198
|
+
// Soft Clip Softness — 0-100 UI (÷50 happens in buildPrimaryCorrectorParams)
|
|
2199
|
+
if (adjustments.softClipHighSoft !== undefined) params.softClipHighSoft = adjustments.softClipHighSoft;
|
|
2200
|
+
if (adjustments.softClipLowSoft !== undefined) params.softClipLowSoft = adjustments.softClipLowSoft;
|
|
2201
|
+
// Contrast Range — 0-1
|
|
2202
|
+
if (adjustments.contrastHighRange !== undefined) params.contrastHighRange = adjustments.contrastHighRange;
|
|
2203
|
+
if (adjustments.contrastLowRange !== undefined) params.contrastLowRange = adjustments.contrastLowRange;
|
|
2204
|
+
// HDR Black Offset — -1 to +1
|
|
2205
|
+
if (adjustments.blackOffset !== undefined) params.blackOffset = adjustments.blackOffset;
|
|
2206
|
+
// Highlights/Shadows sliders — direct values
|
|
2207
|
+
if (adjustments.highlights !== undefined) params.highlights = adjustments.highlights;
|
|
2208
|
+
if (adjustments.shadows !== undefined) params.shadows = adjustments.shadows;
|
|
2209
|
+
// Custom Curves
|
|
2210
|
+
if (adjustments.customCurves) params.customCurves = adjustments.customCurves;
|
|
2211
|
+
// HSL Curves
|
|
2212
|
+
if (adjustments.hslCurves) params.hslCurves = adjustments.hslCurves;
|
|
2213
|
+
|
|
2214
|
+
// Hard clamp all values to valid DRX ranges (same as SIMPLE mode)
|
|
2215
|
+
clampAllParams(params);
|
|
2216
|
+
|
|
2217
|
+
return params;
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2221
|
+
// SIMPLE MODE (backwards compatible — relative adjustments -1 to +1)
|
|
2222
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2223
|
+
|
|
2224
|
+
// Apply temperature shift (warm/cool)
|
|
2225
|
+
// Sets the actual Resolve Temp parameter (-4000 to +4000, unity=0)
|
|
2226
|
+
// Also applies subtle gain/lift tinting for per-channel looks (unless per-channel specified)
|
|
2227
|
+
if (adjustments.temperature) {
|
|
2228
|
+
const t = adjustments.temperature; // -1 (cool) to +1 (warm)
|
|
2229
|
+
// Map to Resolve's Temp parameter range: -4000 to +4000
|
|
2230
|
+
params.temperature = t * 4000;
|
|
2231
|
+
// Also apply subtle gain/lift tinting unless per-channel overrides are present
|
|
2232
|
+
if (!hasPerChannelGain(adjustments)) {
|
|
2233
|
+
params.gain.r += t * 0.1;
|
|
2234
|
+
params.gain.b -= t * 0.1;
|
|
2235
|
+
params.lift.r += t * 0.05;
|
|
2236
|
+
params.lift.b -= t * 0.05;
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// Apply tint shift (green/magenta)
|
|
2241
|
+
// Sets the actual Resolve Tint parameter (-100 to +100, unity=0)
|
|
2242
|
+
if (adjustments.tint) {
|
|
2243
|
+
const t = adjustments.tint; // -1 (green) to +1 (magenta)
|
|
2244
|
+
// Map to Resolve's Tint parameter range: -100 to +100
|
|
2245
|
+
params.tint = t * 100;
|
|
2246
|
+
params.gain.g -= t * 0.1;
|
|
2247
|
+
params.lift.g -= t * 0.05;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// Apply contrast
|
|
2251
|
+
// Resolve Primaries Contrast: floating point, 0.0-2.0, unity=1.0
|
|
2252
|
+
if (adjustments.contrast) {
|
|
2253
|
+
params.contrast = 1.0 + (adjustments.contrast * 1.0);
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Apply saturation
|
|
2257
|
+
// Resolve Primaries Saturation: 0-100, unity=50
|
|
2258
|
+
if (adjustments.saturation) {
|
|
2259
|
+
params.saturation = 50 + (adjustments.saturation * 50);
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
// Apply exposure (overall brightness) - only if per-channel gain master not specified
|
|
2263
|
+
if (adjustments.exposure && adjustments.gainMaster === undefined) {
|
|
2264
|
+
const e = adjustments.exposure;
|
|
2265
|
+
params.gain.master += e;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
// Apply shadow lift - only if per-channel lift not specified
|
|
2269
|
+
if (adjustments.shadowLift && !hasPerChannelLift(adjustments)) {
|
|
2270
|
+
const s = adjustments.shadowLift;
|
|
2271
|
+
params.lift.r += s * 0.1;
|
|
2272
|
+
params.lift.g += s * 0.1;
|
|
2273
|
+
params.lift.b += s * 0.1;
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// Apply highlight compression - only if per-channel gain not specified
|
|
2277
|
+
if (adjustments.highlightCompression && !hasPerChannelGain(adjustments)) {
|
|
2278
|
+
const h = adjustments.highlightCompression;
|
|
2279
|
+
params.gain.r -= h * 0.1;
|
|
2280
|
+
params.gain.g -= h * 0.1;
|
|
2281
|
+
params.gain.b -= h * 0.1;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2285
|
+
// PER-CHANNEL PRIMARY WHEELS (for looks like teal/orange)
|
|
2286
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2287
|
+
|
|
2288
|
+
// Lift (shadows) per-channel
|
|
2289
|
+
if (adjustments.liftR !== undefined) params.lift.r += adjustments.liftR;
|
|
2290
|
+
if (adjustments.liftG !== undefined) params.lift.g += adjustments.liftG;
|
|
2291
|
+
if (adjustments.liftB !== undefined) params.lift.b += adjustments.liftB;
|
|
2292
|
+
if (adjustments.liftMaster !== undefined) params.lift.master += adjustments.liftMaster;
|
|
2293
|
+
|
|
2294
|
+
// Gamma (midtones) per-channel
|
|
2295
|
+
if (adjustments.gammaR !== undefined) params.gamma.r += adjustments.gammaR;
|
|
2296
|
+
if (adjustments.gammaG !== undefined) params.gamma.g += adjustments.gammaG;
|
|
2297
|
+
if (adjustments.gammaB !== undefined) params.gamma.b += adjustments.gammaB;
|
|
2298
|
+
if (adjustments.gammaMaster !== undefined) params.gamma.master += adjustments.gammaMaster;
|
|
2299
|
+
|
|
2300
|
+
// Gain (highlights) per-channel
|
|
2301
|
+
if (adjustments.gainR !== undefined) params.gain.r += adjustments.gainR;
|
|
2302
|
+
if (adjustments.gainG !== undefined) params.gain.g += adjustments.gainG;
|
|
2303
|
+
if (adjustments.gainB !== undefined) params.gain.b += adjustments.gainB;
|
|
2304
|
+
if (adjustments.gainMaster !== undefined) params.gain.master += adjustments.gainMaster;
|
|
2305
|
+
|
|
2306
|
+
// Offset (exposure compensation) per-channel
|
|
2307
|
+
// Note: Tool resolver outputs offsetR/offsetG/offsetB
|
|
2308
|
+
if (adjustments.offsetR !== undefined) params.offset.r += adjustments.offsetR;
|
|
2309
|
+
if (adjustments.offsetG !== undefined) params.offset.g += adjustments.offsetG;
|
|
2310
|
+
if (adjustments.offsetB !== undefined) params.offset.b += adjustments.offsetB;
|
|
2311
|
+
|
|
2312
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2313
|
+
// LOG WHEELS (offset-style grading)
|
|
2314
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2315
|
+
|
|
2316
|
+
// Log Shadow
|
|
2317
|
+
if (adjustments.logShadowR !== undefined) params.logShadow.r = adjustments.logShadowR;
|
|
2318
|
+
if (adjustments.logShadowG !== undefined) params.logShadow.g = adjustments.logShadowG;
|
|
2319
|
+
if (adjustments.logShadowB !== undefined) params.logShadow.b = adjustments.logShadowB;
|
|
2320
|
+
|
|
2321
|
+
// Log Mid
|
|
2322
|
+
if (adjustments.logMidR !== undefined) params.logMid.r = adjustments.logMidR;
|
|
2323
|
+
if (adjustments.logMidG !== undefined) params.logMid.g = adjustments.logMidG;
|
|
2324
|
+
if (adjustments.logMidB !== undefined) params.logMid.b = adjustments.logMidB;
|
|
2325
|
+
|
|
2326
|
+
// Log High
|
|
2327
|
+
if (adjustments.logHighR !== undefined) params.logHigh.r = adjustments.logHighR;
|
|
2328
|
+
if (adjustments.logHighG !== undefined) params.logHigh.g = adjustments.logHighG;
|
|
2329
|
+
if (adjustments.logHighB !== undefined) params.logHigh.b = adjustments.logHighB;
|
|
2330
|
+
|
|
2331
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2332
|
+
// ADDITIONAL CONTROLS
|
|
2333
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2334
|
+
|
|
2335
|
+
// Color Boost (intelligent saturation / vibrance)
|
|
2336
|
+
// DRX AutoResearch 2026-03-17: DRX stores raw value (e.g. 50.0 for UI +50).
|
|
2337
|
+
// User-space in autoresearch experiments: -1 to +1, needs scaling to -100 to +100.
|
|
2338
|
+
if (adjustments.colorBoost !== undefined) {
|
|
2339
|
+
params.colorBoost = adjustments.colorBoost * 100;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// Contrast range controls
|
|
2343
|
+
if (adjustments.contrastHighRange !== undefined) {
|
|
2344
|
+
params.contrastHighRange = adjustments.contrastHighRange;
|
|
2345
|
+
}
|
|
2346
|
+
if (adjustments.contrastLowRange !== undefined) {
|
|
2347
|
+
params.contrastLowRange = adjustments.contrastLowRange;
|
|
2348
|
+
}
|
|
2349
|
+
|
|
2350
|
+
// Pivot (contrast center point)
|
|
2351
|
+
if (adjustments.pivot !== undefined) {
|
|
2352
|
+
params.pivot = adjustments.pivot;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// Midtone detail — scale from user-space (-1 to +1) to Resolve range (-100 to +100)
|
|
2356
|
+
// Matches temperature/tint pattern: all scaling in parseAdjustments, not buildPrimaryCorrectorParams
|
|
2357
|
+
if (adjustments.midtoneDetail !== undefined) {
|
|
2358
|
+
params.midtoneDetail = adjustments.midtoneDetail * 100;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
// Black Offset (HDR global control)
|
|
2362
|
+
// Resolve range: -1.0 to +1.0, default 0. User-space same as Resolve-space.
|
|
2363
|
+
if (adjustments.blackOffset !== undefined) {
|
|
2364
|
+
params.blackOffset = adjustments.blackOffset;
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
// RGB Mixer (3x3 channel mixing matrix)
|
|
2368
|
+
// User provides { rr, gr, br, rg, gg, bg, rb, gb, bb } in DRX-space (identity = diagonal 1.0)
|
|
2369
|
+
if (adjustments.rgbMixer) {
|
|
2370
|
+
params.rgbMixer = adjustments.rgbMixer;
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// Highlights/Shadows sliders (direct values, e.g., 50)
|
|
2374
|
+
if (adjustments.highlights !== undefined) {
|
|
2375
|
+
params.highlights = adjustments.highlights;
|
|
2376
|
+
}
|
|
2377
|
+
if (adjustments.shadows !== undefined) {
|
|
2378
|
+
params.shadows = adjustments.shadows;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// Custom Curves (YRGB) — pass through control points
|
|
2382
|
+
// Format: { y: [{x, y}], r: [{x, y}], g: [{x, y}], b: [{x, y}] }
|
|
2383
|
+
if (adjustments.customCurves) {
|
|
2384
|
+
params.customCurves = adjustments.customCurves;
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2387
|
+
// HSL Curves — pass through control points
|
|
2388
|
+
// Format: { hueVsHue: [{x, y}], hueVsSat: [{x, y}], ... }
|
|
2389
|
+
if (adjustments.hslCurves) {
|
|
2390
|
+
params.hslCurves = adjustments.hslCurves;
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2394
|
+
// HARD CLAMP — prevent out-of-range values from reaching the DRX encoder.
|
|
2395
|
+
// Uses parameter-ranges.js as the single source of truth for valid ranges.
|
|
2396
|
+
// This catches overflow from additive stacking (temperature + exposure on gain,
|
|
2397
|
+
// tint side-effects on lift, etc.) and out-of-range user inputs.
|
|
2398
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2399
|
+
clampAllParams(params);
|
|
2400
|
+
|
|
2401
|
+
return params;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
/**
|
|
2405
|
+
* Check if per-channel lift parameters are specified
|
|
2406
|
+
*/
|
|
2407
|
+
function hasPerChannelLift(adjustments) {
|
|
2408
|
+
return adjustments.liftR !== undefined ||
|
|
2409
|
+
adjustments.liftG !== undefined ||
|
|
2410
|
+
adjustments.liftB !== undefined ||
|
|
2411
|
+
adjustments.liftMaster !== undefined;
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2415
|
+
// STUB ENCODING FUNCTIONS (Awaiting DRX Training)
|
|
2416
|
+
// These functions are ready to encode when parameter IDs are discovered
|
|
2417
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2418
|
+
|
|
2419
|
+
/**
|
|
2420
|
+
* Check if a parameter ID is available (not null/undefined)
|
|
2421
|
+
* @param {*} paramId - Parameter ID to check
|
|
2422
|
+
* @param {string} name - Human-readable name for logging
|
|
2423
|
+
* @returns {boolean}
|
|
2424
|
+
*/
|
|
2425
|
+
function isParamIdAvailable(paramId, name) {
|
|
2426
|
+
if (paramId === null || paramId === undefined) {
|
|
2427
|
+
console.warn(`[DRX] Parameter ID not yet captured: ${name} (needs DRX training sample)`);
|
|
2428
|
+
return false;
|
|
2429
|
+
}
|
|
2430
|
+
return true;
|
|
2431
|
+
}
|
|
2432
|
+
|
|
2433
|
+
/**
|
|
2434
|
+
* Encode a single HDR zone's nested structure
|
|
2435
|
+
* HDR zones use nested protobuf with zone name + exposure/saturation/hue fields
|
|
2436
|
+
*
|
|
2437
|
+
* @param {string} zoneName - Zone name ("Dark", "Shadow", "Light", "Highlight", "Global")
|
|
2438
|
+
* @param {Object} zoneParams - { exposure, saturation, colorBalanceX, colorBalanceY }
|
|
2439
|
+
* (also accepts legacy hueAngle/hueSat as aliases for colorBalanceY/colorBalanceX)
|
|
2440
|
+
* @returns {Buffer} - Encoded zone data
|
|
2441
|
+
*/
|
|
2442
|
+
function encodeHDRZoneData(zoneName, zoneParams) {
|
|
2443
|
+
const parts = [];
|
|
2444
|
+
const { HDR_ZONE } = drxParams;
|
|
2445
|
+
|
|
2446
|
+
// Zone name string (wire type 0x0a = length-delimited)
|
|
2447
|
+
const nameBytes = Buffer.from(zoneName, 'utf8');
|
|
2448
|
+
parts.push(Buffer.from([HDR_ZONE.WIRE.ZONE_NAME, nameBytes.length]));
|
|
2449
|
+
parts.push(nameBytes);
|
|
2450
|
+
|
|
2451
|
+
// Exposure (wire type 0x15 = fixed32)
|
|
2452
|
+
if (zoneParams.exposure !== undefined && zoneParams.exposure !== 0) {
|
|
2453
|
+
const expBuf = Buffer.alloc(5);
|
|
2454
|
+
expBuf[0] = HDR_ZONE.WIRE.EXPOSURE;
|
|
2455
|
+
expBuf.writeFloatLE(zoneParams.exposure, 1);
|
|
2456
|
+
parts.push(expBuf);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// Color Balance Y (wire type 0x1d = F3 fixed32)
|
|
2460
|
+
// Accept both new name (colorBalanceY) and legacy (hueAngle)
|
|
2461
|
+
const cbY = zoneParams.colorBalanceY ?? zoneParams.hueAngle;
|
|
2462
|
+
if (cbY !== undefined && cbY !== 0) {
|
|
2463
|
+
const cbYBuf = Buffer.alloc(5);
|
|
2464
|
+
cbYBuf[0] = HDR_ZONE.WIRE.COLOR_BALANCE_Y;
|
|
2465
|
+
cbYBuf.writeFloatLE(cbY, 1);
|
|
2466
|
+
parts.push(cbYBuf);
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
// Color Balance X (wire type 0x25 = F4 fixed32)
|
|
2470
|
+
// Accept both new name (colorBalanceX) and legacy (hueSat)
|
|
2471
|
+
const cbX = zoneParams.colorBalanceX ?? zoneParams.hueSat;
|
|
2472
|
+
if (cbX !== undefined && cbX !== 0) {
|
|
2473
|
+
const cbXBuf = Buffer.alloc(5);
|
|
2474
|
+
cbXBuf[0] = HDR_ZONE.WIRE.COLOR_BALANCE_X;
|
|
2475
|
+
cbXBuf.writeFloatLE(cbX, 1);
|
|
2476
|
+
parts.push(cbXBuf);
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// Falloff (wire type 0x2d = F5 fixed32, default 0.2)
|
|
2480
|
+
// Confirmed 2026-03-18: Dark zone falloff=0.50 → field 5 = 0.5, default field 4 = 0.2
|
|
2481
|
+
if (zoneParams.falloff !== undefined) {
|
|
2482
|
+
const falloffBuf = Buffer.alloc(5);
|
|
2483
|
+
falloffBuf[0] = 0x2d; // Field 5, wire type 5 (fixed32)
|
|
2484
|
+
falloffBuf.writeFloatLE(zoneParams.falloff, 1);
|
|
2485
|
+
parts.push(falloffBuf);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
// Saturation (wire type 0x3d = fixed32)
|
|
2489
|
+
// FIXED 2026-01-17: Always include saturation even at default (1.0) - Resolve expects it
|
|
2490
|
+
const satValue = zoneParams.saturation !== undefined ? zoneParams.saturation : 1.0;
|
|
2491
|
+
const satBuf = Buffer.alloc(5);
|
|
2492
|
+
satBuf[0] = HDR_ZONE.WIRE.SATURATION;
|
|
2493
|
+
satBuf.writeFloatLE(satValue, 1);
|
|
2494
|
+
parts.push(satBuf);
|
|
2495
|
+
|
|
2496
|
+
return Buffer.concat(parts);
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
/**
|
|
2500
|
+
* Build HDR Wheel parameters using nested protobuf structure
|
|
2501
|
+
* TRAINED 2026-01-14 from 13 HDR DRX samples
|
|
2502
|
+
*
|
|
2503
|
+
* HDR Zones use a fundamentally different encoding than standard parameters:
|
|
2504
|
+
* - Zone adjustments container (0x86000305): exposure, color balance X/Y, saturation
|
|
2505
|
+
* - Zone definitions container (0x86000306): range boundaries, falloff
|
|
2506
|
+
* - Zone differentiation via embedded zone name strings
|
|
2507
|
+
*
|
|
2508
|
+
* @param {Object} colorParams - Color parameters containing hdrDark, hdrShadow, etc.
|
|
2509
|
+
* @returns {Array} - Array of parameter entries with nested zone data
|
|
2510
|
+
*/
|
|
2511
|
+
function buildHDRWheelParams(colorParams) {
|
|
2512
|
+
const paramEntries = [];
|
|
2513
|
+
const { HDR_ZONE } = drxParams;
|
|
2514
|
+
|
|
2515
|
+
// Map semantic names to DRX zone names
|
|
2516
|
+
const zoneMapping = {
|
|
2517
|
+
hdrBlack: HDR_ZONE.ZONE_NAMES.BLACK,
|
|
2518
|
+
hdrDark: HDR_ZONE.ZONE_NAMES.DARK,
|
|
2519
|
+
hdrShadow: HDR_ZONE.ZONE_NAMES.SHADOW,
|
|
2520
|
+
hdrLight: HDR_ZONE.ZONE_NAMES.LIGHT,
|
|
2521
|
+
hdrHighlight: HDR_ZONE.ZONE_NAMES.HIGHLIGHT,
|
|
2522
|
+
hdrSpecular: HDR_ZONE.ZONE_NAMES.SPECULAR,
|
|
2523
|
+
hdrGlobal: HDR_ZONE.ZONE_NAMES.GLOBAL,
|
|
2524
|
+
};
|
|
2525
|
+
|
|
2526
|
+
// Check each zone and encode if it has non-default values
|
|
2527
|
+
for (const [semanticKey, zoneName] of Object.entries(zoneMapping)) {
|
|
2528
|
+
const zoneParams = colorParams[semanticKey];
|
|
2529
|
+
if (!zoneParams) continue;
|
|
2530
|
+
|
|
2531
|
+
// Check if zone has any non-default values
|
|
2532
|
+
const hasExposure = zoneParams.exposure !== undefined && zoneParams.exposure !== 0;
|
|
2533
|
+
const hasSaturation = zoneParams.saturation !== undefined && zoneParams.saturation !== 1.0;
|
|
2534
|
+
const hasCBX = (zoneParams.colorBalanceX ?? zoneParams.hueSat) !== undefined && (zoneParams.colorBalanceX ?? zoneParams.hueSat ?? 0) !== 0;
|
|
2535
|
+
const hasCBY = (zoneParams.colorBalanceY ?? zoneParams.hueAngle) !== undefined && (zoneParams.colorBalanceY ?? zoneParams.hueAngle ?? 0) !== 0;
|
|
2536
|
+
|
|
2537
|
+
if (hasExposure || hasSaturation || hasCBX || hasCBY) {
|
|
2538
|
+
// Encode this zone's nested structure
|
|
2539
|
+
const zoneData = encodeHDRZoneData(zoneName, zoneParams);
|
|
2540
|
+
|
|
2541
|
+
// Wrap in parameter entry with ZONE_DATA ID
|
|
2542
|
+
// The createParameterEntry function needs to handle this specially for nested data
|
|
2543
|
+
paramEntries.push({
|
|
2544
|
+
paramId: HDR_ZONE.ZONE_DATA,
|
|
2545
|
+
nestedData: zoneData,
|
|
2546
|
+
zoneName: zoneName,
|
|
2547
|
+
isNested: true,
|
|
2548
|
+
});
|
|
2549
|
+
|
|
2550
|
+
debugLog(`[DRX] HDR Zone ${zoneName}: exp=${zoneParams.exposure || 0}, sat=${zoneParams.saturation || 1}, cbX=${zoneParams.colorBalanceX || 0}, cbY=${zoneParams.colorBalanceY || 0}`);
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
if (paramEntries.length > 0) {
|
|
2555
|
+
debugLog('[DRX] HDR Wheel zones built:', paramEntries.length);
|
|
2556
|
+
}
|
|
2557
|
+
return paramEntries;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
/**
|
|
2561
|
+
* Build HSL Qualifier parameters (STUB - awaiting training)
|
|
2562
|
+
* Will encode once HSL_QUALIFIER parameter IDs are captured from DRX samples
|
|
2563
|
+
*
|
|
2564
|
+
* @param {Object} qualifierParams - Qualifier settings (hueCenter, hueWidth, satLow, etc.)
|
|
2565
|
+
* @returns {Array} - Array of parameter entries (empty until trained)
|
|
2566
|
+
*/
|
|
2567
|
+
/**
|
|
2568
|
+
* Build HSL Qualifier parameters (Corrector Type 2).
|
|
2569
|
+
*
|
|
2570
|
+
* All values should be in UI units (0-100 for percentages, 0-360 for hue).
|
|
2571
|
+
* Internally converts to DRX encoding (÷100).
|
|
2572
|
+
*
|
|
2573
|
+
* TRAINED 2026-03-22: All 12 qualifier IDs + mode flag + softness confirmed.
|
|
2574
|
+
*
|
|
2575
|
+
* @param {Object} qualifierParams - Qualifier settings in UI values
|
|
2576
|
+
* @param {number} [qualifierParams.hueCenter] - Hue center (0-100, maps to 0-360°)
|
|
2577
|
+
* @param {number} [qualifierParams.hueWidth] - Hue width
|
|
2578
|
+
* @param {number} [qualifierParams.hueSoft] - Hue edge softness
|
|
2579
|
+
* @param {number} [qualifierParams.hueSymmetry=50] - Hue symmetry
|
|
2580
|
+
* @param {number} [qualifierParams.satLow] - Saturation low threshold
|
|
2581
|
+
* @param {number} [qualifierParams.satHigh] - Saturation high threshold
|
|
2582
|
+
* @param {number} [qualifierParams.satLowSoft] - Sat low softness
|
|
2583
|
+
* @param {number} [qualifierParams.satHighSoft] - Sat high softness
|
|
2584
|
+
* @param {number} [qualifierParams.lumLow] - Luminance low threshold
|
|
2585
|
+
* @param {number} [qualifierParams.lumHigh] - Luminance high threshold
|
|
2586
|
+
* @param {number} [qualifierParams.lumLowSoft] - Lum low softness
|
|
2587
|
+
* @param {number} [qualifierParams.lumHighSoft] - Lum high softness
|
|
2588
|
+
* @param {number} [qualifierParams.blur] - Blur radius (on primary corrector)
|
|
2589
|
+
* @returns {Array} - Parameter entries for corrector type 2
|
|
2590
|
+
*/
|
|
2591
|
+
function buildQualifierParams(qualifierParams) {
|
|
2592
|
+
const paramEntries = [];
|
|
2593
|
+
const { HSL_QUALIFIER } = drxParams;
|
|
2594
|
+
|
|
2595
|
+
if (!qualifierParams) return paramEntries;
|
|
2596
|
+
|
|
2597
|
+
// All qualifier params encode as UI ÷ 100
|
|
2598
|
+
const q = qualifierParams;
|
|
2599
|
+
const add = (id, val) => {
|
|
2600
|
+
if (val !== undefined) paramEntries.push(createParameterEntry(id, val / 100));
|
|
2601
|
+
};
|
|
2602
|
+
|
|
2603
|
+
// Hue selection
|
|
2604
|
+
add(HSL_QUALIFIER.HUE_CENTER, q.hueCenter);
|
|
2605
|
+
add(HSL_QUALIFIER.HUE_WIDTH, q.hueWidth);
|
|
2606
|
+
add(HSL_QUALIFIER.HUE_SYM, q.hueSymmetry !== undefined ? q.hueSymmetry : (q.hueCenter !== undefined ? 50 : undefined));
|
|
2607
|
+
add(HSL_QUALIFIER.HUE_SOFT, q.hueSoft);
|
|
2608
|
+
|
|
2609
|
+
// Saturation selection
|
|
2610
|
+
add(HSL_QUALIFIER.SAT_HIGH, q.satHigh);
|
|
2611
|
+
add(HSL_QUALIFIER.SAT_LOW, q.satLow);
|
|
2612
|
+
add(HSL_QUALIFIER.SAT_HIGH_SOFT, q.satHighSoft);
|
|
2613
|
+
add(HSL_QUALIFIER.SAT_LOW_SOFT, q.satLowSoft);
|
|
2614
|
+
|
|
2615
|
+
// Luminance selection
|
|
2616
|
+
add(HSL_QUALIFIER.LUM_HIGH, q.lumHigh);
|
|
2617
|
+
add(HSL_QUALIFIER.LUM_LOW, q.lumLow);
|
|
2618
|
+
add(HSL_QUALIFIER.LUM_HIGH_SOFT, q.lumHighSoft);
|
|
2619
|
+
add(HSL_QUALIFIER.LUM_LOW_SOFT, q.lumLowSoft);
|
|
2620
|
+
|
|
2621
|
+
// Duplicate hue params (Resolve stores these)
|
|
2622
|
+
if (q.hueWidth !== undefined) add(HSL_QUALIFIER.HUE_WIDTH_DUP, q.hueWidth);
|
|
2623
|
+
if (q.hueSoft !== undefined) add(HSL_QUALIFIER.HUE_SOFT_DUP, q.hueSoft);
|
|
2624
|
+
|
|
2625
|
+
// Duplicate hue params removed from above — already handled
|
|
2626
|
+
|
|
2627
|
+
// Mode flags (wire-confirmed 2026-07-01): BOTH are varint envelopes {F2: n} in live
|
|
2628
|
+
// data — MODE_FLAG (0x0830006F) observed 4; QUALIFIER_MODE (0x88300001) carries the
|
|
2629
|
+
// qualifier mode (HSL=0, RGB=2, luma=4, 3D=6). The old float32 encoding decoded as
|
|
2630
|
+
// null/garbage and was wire-unfaithful.
|
|
2631
|
+
if (paramEntries.length > 0) {
|
|
2632
|
+
paramEntries.push(createVarintParameterEntry(HSL_QUALIFIER.MODE_FLAG, 4));
|
|
2633
|
+
paramEntries.push(createVarintParameterEntry(HSL_QUALIFIER.QUALIFIER_MODE, 0));
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
if (paramEntries.length > 0) {
|
|
2637
|
+
debugLog('[DRX] HSL Qualifier params built:', paramEntries.length);
|
|
2638
|
+
}
|
|
2639
|
+
return paramEntries;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
/**
|
|
2643
|
+
* Build RGB Qualifier parameters (Corrector Type 2, Mode 2).
|
|
2644
|
+
*
|
|
2645
|
+
* Uses same corrector type as HSL but with QUALIFIER_MODE=2.
|
|
2646
|
+
* Params at 0x08300002-0x0830000D for per-channel R/G/B Low/High/Soft.
|
|
2647
|
+
* All values in UI units (0-100), internally converts ÷100.
|
|
2648
|
+
*
|
|
2649
|
+
* TRAINED 2026-03-16: IDs confirmed in calibration session.
|
|
2650
|
+
*
|
|
2651
|
+
* @param {Object} rgbParams - RGB qualifier settings
|
|
2652
|
+
* @param {number} [rgbParams.rLow] - Red low threshold
|
|
2653
|
+
* @param {number} [rgbParams.rHigh] - Red high threshold
|
|
2654
|
+
* @param {number} [rgbParams.rLowSoft] - Red low softness
|
|
2655
|
+
* @param {number} [rgbParams.rHighSoft] - Red high softness
|
|
2656
|
+
* @param {number} [rgbParams.gLow] - Green low threshold (etc.)
|
|
2657
|
+
* @param {number} [rgbParams.bLow] - Blue low threshold (etc.)
|
|
2658
|
+
* @returns {Array} - Parameter entries for corrector type 2 with mode=2
|
|
2659
|
+
*/
|
|
2660
|
+
function buildRGBQualifierParams(rgbParams) {
|
|
2661
|
+
const paramEntries = [];
|
|
2662
|
+
const { HSL_QUALIFIER } = drxParams;
|
|
2663
|
+
|
|
2664
|
+
if (!rgbParams) return paramEntries;
|
|
2665
|
+
|
|
2666
|
+
const q = rgbParams;
|
|
2667
|
+
const add = (id, val) => {
|
|
2668
|
+
if (val !== undefined) paramEntries.push(createParameterEntry(id, val / 100));
|
|
2669
|
+
};
|
|
2670
|
+
|
|
2671
|
+
// Red channel
|
|
2672
|
+
add(HSL_QUALIFIER.RGB_R_LOW, q.rLow);
|
|
2673
|
+
add(HSL_QUALIFIER.RGB_R_HIGH, q.rHigh);
|
|
2674
|
+
add(HSL_QUALIFIER.RGB_R_LOW_SOFT, q.rLowSoft);
|
|
2675
|
+
add(HSL_QUALIFIER.RGB_R_HIGH_SOFT, q.rHighSoft);
|
|
2676
|
+
|
|
2677
|
+
// Green channel
|
|
2678
|
+
add(HSL_QUALIFIER.RGB_G_LOW, q.gLow);
|
|
2679
|
+
add(HSL_QUALIFIER.RGB_G_HIGH, q.gHigh);
|
|
2680
|
+
add(HSL_QUALIFIER.RGB_G_LOW_SOFT, q.gLowSoft);
|
|
2681
|
+
add(HSL_QUALIFIER.RGB_G_HIGH_SOFT, q.gHighSoft);
|
|
2682
|
+
|
|
2683
|
+
// Blue channel
|
|
2684
|
+
add(HSL_QUALIFIER.RGB_B_LOW, q.bLow);
|
|
2685
|
+
add(HSL_QUALIFIER.RGB_B_HIGH, q.bHigh);
|
|
2686
|
+
add(HSL_QUALIFIER.RGB_B_LOW_SOFT, q.bLowSoft);
|
|
2687
|
+
add(HSL_QUALIFIER.RGB_B_HIGH_SOFT, q.bHighSoft);
|
|
2688
|
+
|
|
2689
|
+
// Mode = RGB (2) — varint envelope (wire-confirmed: qualifier-rgb fixture {F2:2})
|
|
2690
|
+
if (paramEntries.length > 0) {
|
|
2691
|
+
paramEntries.push(createVarintParameterEntry(HSL_QUALIFIER.QUALIFIER_MODE, 2));
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
if (paramEntries.length > 0) {
|
|
2695
|
+
debugLog('[DRX] RGB Qualifier params built:', paramEntries.length);
|
|
2696
|
+
}
|
|
2697
|
+
return paramEntries;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
/**
|
|
2701
|
+
* Build Luma Qualifier parameters (Corrector Type 2, Mode 4).
|
|
2702
|
+
*
|
|
2703
|
+
* Uses same corrector type with QUALIFIER_MODE=4.
|
|
2704
|
+
* Reuses Lum Low/High/Soft params from HSL qualifier.
|
|
2705
|
+
* All values in UI units (0-100), internally converts ÷100.
|
|
2706
|
+
*
|
|
2707
|
+
* @param {Object} lumaParams - Luma qualifier settings
|
|
2708
|
+
* @param {number} [lumaParams.lumLow] - Luminance low threshold
|
|
2709
|
+
* @param {number} [lumaParams.lumHigh] - Luminance high threshold
|
|
2710
|
+
* @param {number} [lumaParams.lumLowSoft] - Lum low softness
|
|
2711
|
+
* @param {number} [lumaParams.lumHighSoft] - Lum high softness
|
|
2712
|
+
* @returns {Array} - Parameter entries for corrector type 2 with mode=4
|
|
2713
|
+
*/
|
|
2714
|
+
function buildLumaQualifierParams(lumaParams) {
|
|
2715
|
+
const paramEntries = [];
|
|
2716
|
+
const { HSL_QUALIFIER } = drxParams;
|
|
2717
|
+
|
|
2718
|
+
if (!lumaParams) return paramEntries;
|
|
2719
|
+
|
|
2720
|
+
const q = lumaParams;
|
|
2721
|
+
const add = (id, val) => {
|
|
2722
|
+
if (val !== undefined) paramEntries.push(createParameterEntry(id, val / 100));
|
|
2723
|
+
};
|
|
2724
|
+
|
|
2725
|
+
add(HSL_QUALIFIER.LUM_HIGH, q.lumHigh);
|
|
2726
|
+
add(HSL_QUALIFIER.LUM_LOW, q.lumLow);
|
|
2727
|
+
add(HSL_QUALIFIER.LUM_HIGH_SOFT, q.lumHighSoft);
|
|
2728
|
+
add(HSL_QUALIFIER.LUM_LOW_SOFT, q.lumLowSoft);
|
|
2729
|
+
|
|
2730
|
+
// Mode = Luma (4) — varint envelope (same wire form as the confirmed HSL/RGB modes)
|
|
2731
|
+
if (paramEntries.length > 0) {
|
|
2732
|
+
paramEntries.push(createVarintParameterEntry(HSL_QUALIFIER.QUALIFIER_MODE, 4));
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
if (paramEntries.length > 0) {
|
|
2736
|
+
debugLog('[DRX] Luma Qualifier params built:', paramEntries.length);
|
|
2737
|
+
}
|
|
2738
|
+
return paramEntries;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
/**
|
|
2742
|
+
* Build Power Window parameters (STUB - awaiting training)
|
|
2743
|
+
* Will encode once POWER_WINDOWS parameter IDs are captured from DRX samples
|
|
2744
|
+
*
|
|
2745
|
+
* @param {Object} windowParams - Window settings (type, centerX, centerY, width, etc.)
|
|
2746
|
+
* @returns {Array} - Array of parameter entries (empty until trained)
|
|
2747
|
+
*/
|
|
2748
|
+
/**
|
|
2749
|
+
* Build Power Window parameters (Corrector Type 4 for circle, 65539 for linear, 65554 for gradient).
|
|
2750
|
+
*
|
|
2751
|
+
* TRAINED 2026-03-22: All window types confirmed with correct scaling.
|
|
2752
|
+
* - Circle: type 4, pan/tilt in pixels, softness via SoftRef (UI × 16)
|
|
2753
|
+
* - Linear: type 65539 (0x10003), Soft1-4 in 0x0870xxxx range (UI × 16)
|
|
2754
|
+
* - Gradient: type 65554 (0x10012), 0x08F0xxxx range, softness (UI × 100)
|
|
2755
|
+
*
|
|
2756
|
+
* @param {Object} windowParams - Window settings in UI values
|
|
2757
|
+
* @param {number} windowParams.type - 1=Circle, 2=Linear, 5=Gradient
|
|
2758
|
+
* @param {number} [windowParams.pan=50] - Horizontal position (UI 0-100, 50=center)
|
|
2759
|
+
* @param {number} [windowParams.tilt=50] - Vertical position (UI 0-100, 50=center)
|
|
2760
|
+
* @param {number} [windowParams.size=50] - Size (UI 0-100, 50=default)
|
|
2761
|
+
* @param {number} [windowParams.aspect=50] - Aspect ratio (UI 0-100, 50=1:1)
|
|
2762
|
+
* @param {number} [windowParams.rotate=0] - Rotation degrees
|
|
2763
|
+
* @param {number} [windowParams.soft1=0] - Softness 1
|
|
2764
|
+
* @param {number} [windowParams.soft2=0] - Softness 2 (linear only)
|
|
2765
|
+
* @param {number} [windowParams.soft3=0] - Softness 3 (linear only)
|
|
2766
|
+
* @param {number} [windowParams.soft4=0] - Softness 4 (linear only)
|
|
2767
|
+
* @returns {Array} - Parameter entries for window corrector
|
|
2768
|
+
*/
|
|
2769
|
+
function buildWindowParams(windowParams) {
|
|
2770
|
+
const groups = { transform: [], softMask: [], gradient: [], shape: [] };
|
|
2771
|
+
const paramEntries = groups.transform;
|
|
2772
|
+
const { POWER_WINDOWS } = drxParams;
|
|
2773
|
+
|
|
2774
|
+
if (!windowParams || windowParams.type === 0) return groups;
|
|
2775
|
+
|
|
2776
|
+
const w = windowParams;
|
|
2777
|
+
|
|
2778
|
+
// SHAPE MODEL (wire-confirmed 2026-07-01 against the live fixtures): Resolve does NOT
|
|
2779
|
+
// discriminate window shape via 0x88500008 — that flag is a CONSTANT varint {F2:2} in
|
|
2780
|
+
// every shape's fixture (circle/linear/gradient/polygon/curve). Shape is expressed by
|
|
2781
|
+
// WHICH corrector blocks exist: circle = ct4 transform only; linear = ct4 + ct3
|
|
2782
|
+
// softness mask (0x0870xxxx, UI×16); gradient = ct65554 (0x08F0xxxx); polygon/curve =
|
|
2783
|
+
// ct4 + ct6 vertex ring (no write support — decode-only).
|
|
2784
|
+
// The ct4 type flag accompanies circle/linear windows. For vertex shapes (polygon/
|
|
2785
|
+
// curve) it spawns a SEPARATE empty circle window row (live-observed 2026-07-02), so
|
|
2786
|
+
// skip it — the ct6 shape block alone should define the window.
|
|
2787
|
+
if (w.type !== undefined && w.type !== 5 && !(w.type === 3 || w.type === 4)) {
|
|
2788
|
+
paramEntries.push(createVarintParameterEntry(POWER_WINDOWS.WINDOW_TYPE, 2));
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// TRUE transform scales — live multi-point fit 2026-06-22 (§16a), applied to the
|
|
2792
|
+
// generator 2026-07-01 after the registry window ranges were widened to the real
|
|
2793
|
+
// DRX-internal spans (they previously clamped pan/tilt/softRef to ±1, which is why
|
|
2794
|
+
// the generator had kept placeholder conventions). The same scales are locked in
|
|
2795
|
+
// test/power-window-transform-calibration.test.mjs and mirrored by
|
|
2796
|
+
// extract-power-window.js, so UI-in → UI-out round-trips AND matches Resolve.
|
|
2797
|
+
// rotate = −UI°/180 · aspect = (50−UI)/50 · size = 1+(UI−50)×0.08 ·
|
|
2798
|
+
// pan/tilt = (UI−50)/50 × 4096 (frame-pixel space).
|
|
2799
|
+
|
|
2800
|
+
const isGradient = w.type === 5;
|
|
2801
|
+
const isLinear = w.type === 2;
|
|
2802
|
+
const isVertexShape = (w.type === 3 || w.type === 4) && Array.isArray(w.vertices) && w.vertices.length >= 3;
|
|
2803
|
+
|
|
2804
|
+
if (isVertexShape) {
|
|
2805
|
+
// Polygon (3) / Curve (4) windows carry their freeform shape in a ct6 block
|
|
2806
|
+
// (live-RE'd 2026-06-22, write path added 2026-07-02 — PENDING live rig verify):
|
|
2807
|
+
// 0x08D00002 varint {F2:2} · 0x08D00004 = 0 · three vertex RINGS at
|
|
2808
|
+
// 0x08D00006/07/08 (value envelope F9.F1[] of f32 {x,y} points, FRAME PIXELS from
|
|
2809
|
+
// center) · 0x08D00009/0A = 0 · 0x08D0000B/0C = 1 · identity 3×3 at 0x88D00014
|
|
2810
|
+
// (F10.F1–F9). Ring = bezier corner repetition: first corner ×2, others ×3,
|
|
2811
|
+
// closed with the first corner ×2 (4-corner fixture → 13 points, all three rings
|
|
2812
|
+
// identical for straight edges). 0x08D00010/11 (unknown floats) omitted in v1.
|
|
2813
|
+
const s = groups.shape;
|
|
2814
|
+
const ring = [];
|
|
2815
|
+
w.vertices.forEach((v, i) => {
|
|
2816
|
+
const reps = i === 0 ? 2 : 3;
|
|
2817
|
+
for (let r = 0; r < reps; r++) ring.push(createCurvePoint(v.x, v.y));
|
|
2818
|
+
});
|
|
2819
|
+
ring.push(createCurvePoint(w.vertices[0].x, w.vertices[0].y));
|
|
2820
|
+
ring.push(createCurvePoint(w.vertices[0].x, w.vertices[0].y));
|
|
2821
|
+
const ringEnvelope = protoBytes(9, Buffer.concat(ring)); // F9 { F1[] points }
|
|
2822
|
+
s.push(createVarintParameterEntry(0x08d00002, 2));
|
|
2823
|
+
s.push(createParameterEntry(0x08d00004, 0));
|
|
2824
|
+
for (const ringId of [0x08d00006, 0x08d00007, 0x08d00008]) {
|
|
2825
|
+
s.push(protoBytes(3, Buffer.concat([protoVarint(1, ringId), protoBytes(2, ringEnvelope)])));
|
|
2826
|
+
}
|
|
2827
|
+
s.push(createParameterEntry(0x08d00009, 0));
|
|
2828
|
+
s.push(createParameterEntry(0x08d0000a, 0));
|
|
2829
|
+
s.push(createParameterEntry(0x08d0000b, 1));
|
|
2830
|
+
s.push(createParameterEntry(0x08d0000c, 1));
|
|
2831
|
+
const IDENTITY = [1, 0, 0, 0, 1, 0, 0, 0, 1];
|
|
2832
|
+
const matrixMsg = Buffer.concat(IDENTITY.map((v, i) => protoFloat32(i + 1, v)));
|
|
2833
|
+
s.push(protoBytes(3, Buffer.concat([protoVarint(1, 0x88d00014), protoBytes(2, protoBytes(10, matrixMsg))])));
|
|
2834
|
+
debugLog('[DRX] Window vertex shape (ct6):', w.vertices.length, 'corners →', 13, 'ring points');
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
if (isGradient) {
|
|
2838
|
+
// Gradient windows live ENTIRELY in a ct65554 block (0x08F0xxxx). The old code wrote
|
|
2839
|
+
// these ids into the ct4 transform block, where live data never puts them.
|
|
2840
|
+
const GW = drxParams.GRADIENT_WINDOW;
|
|
2841
|
+
const g = groups.gradient;
|
|
2842
|
+
g.push(createVarintParameterEntry(GW.TYPE, 2));
|
|
2843
|
+
if (w.rotate !== undefined && w.rotate !== 0) {
|
|
2844
|
+
g.push(createParameterEntry(GW.ROTATION, -w.rotate / 180));
|
|
2845
|
+
}
|
|
2846
|
+
// Handle positions carry the gradient's placement in the same (UI−50)/50 × 4096
|
|
2847
|
+
// frame-pixel space as the ct4 pan/tilt (fixture: Pan 81 → 2539.52, Tilt 82 → 2621.44).
|
|
2848
|
+
if (w.pan !== undefined && w.pan !== 50) {
|
|
2849
|
+
g.push(createParameterEntry(GW.HANDLE_1_POS, ((w.pan - 50) / 50) * 4096));
|
|
2850
|
+
}
|
|
2851
|
+
if (w.tilt !== undefined && w.tilt !== 50) {
|
|
2852
|
+
g.push(createParameterEntry(GW.HANDLE_2_POS, ((w.tilt - 50) / 50) * 4096));
|
|
2853
|
+
}
|
|
2854
|
+
if (w.soft1 !== undefined && w.soft1 !== 0) {
|
|
2855
|
+
g.push(createParameterEntry(GW.SOFTNESS, w.soft1 * 100));
|
|
2856
|
+
}
|
|
2857
|
+
if (w.opacity !== undefined && w.opacity !== 100) {
|
|
2858
|
+
g.push(createParameterEntry(GW.OPACITY, w.opacity / 100));
|
|
2859
|
+
}
|
|
2860
|
+
debugLog('[DRX] Window params built:', g.length, '(gradient, ct65554)');
|
|
2861
|
+
return groups;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
// Transform: rotation, stored = −UI°/180 (neutral 0)
|
|
2865
|
+
if (w.rotate !== undefined && w.rotate !== 0) {
|
|
2866
|
+
paramEntries.push(createParameterEntry(POWER_WINDOWS.ROTATE, -w.rotate / 180));
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
// Size: UI 50 = default = DRX 1.0; stored = 1 + (UI−50)×0.08
|
|
2870
|
+
if (w.size !== undefined && w.size !== 50) {
|
|
2871
|
+
paramEntries.push(createParameterEntry(POWER_WINDOWS.SIZE, 1 + (w.size - 50) * 0.08));
|
|
2872
|
+
}
|
|
2873
|
+
|
|
2874
|
+
// Aspect: UI 50 = 1:1 = DRX 0.0; stored = (50−UI)/50
|
|
2875
|
+
if (w.aspect !== undefined && w.aspect !== 50) {
|
|
2876
|
+
paramEntries.push(createParameterEntry(POWER_WINDOWS.ASPECT, (50 - w.aspect) / 50));
|
|
2877
|
+
}
|
|
2878
|
+
|
|
2879
|
+
// Pan/Tilt: stored = (UI−50)/50 × 4096 (frame-pixel offset from center)
|
|
2880
|
+
if (w.pan !== undefined && w.pan !== 50) {
|
|
2881
|
+
paramEntries.push(createParameterEntry(POWER_WINDOWS.PAN, ((w.pan - 50) / 50) * 4096));
|
|
2882
|
+
}
|
|
2883
|
+
if (w.tilt !== undefined && w.tilt !== 50) {
|
|
2884
|
+
paramEntries.push(createParameterEntry(POWER_WINDOWS.TILT, ((w.tilt - 50) / 50) * 4096));
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// Opacity: /100 (live-confirmed: UI 66 → 0.66)
|
|
2888
|
+
if (w.opacity !== undefined && w.opacity !== 100) {
|
|
2889
|
+
paramEntries.push(createParameterEntry(POWER_WINDOWS.OPACITY, w.opacity / 100));
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
// Softness — UI × 16 (live-confirmed both circle SoftRef and linear Soft1–4).
|
|
2893
|
+
if (isLinear) {
|
|
2894
|
+
// Linear softness mask lives in a ct3 block (0x08700009–0C), NOT the ct4 transform
|
|
2895
|
+
// (linear-window-softness fixture: ct3, ÷16 exact). The old code wrote Soft1–4 into ct4.
|
|
2896
|
+
const m = groups.softMask;
|
|
2897
|
+
if (w.soft1 !== undefined && w.soft1 !== 0) m.push(createParameterEntry(POWER_WINDOWS.SOFT_1, w.soft1 * 16));
|
|
2898
|
+
if (w.soft2 !== undefined && w.soft2 !== 0) m.push(createParameterEntry(POWER_WINDOWS.SOFT_2, w.soft2 * 16));
|
|
2899
|
+
if (w.soft3 !== undefined && w.soft3 !== 0) m.push(createParameterEntry(POWER_WINDOWS.SOFT_3, w.soft3 * 16));
|
|
2900
|
+
if (w.soft4 !== undefined && w.soft4 !== 0) m.push(createParameterEntry(POWER_WINDOWS.SOFT_4, w.soft4 * 16));
|
|
2901
|
+
} else if (w.soft1 !== undefined && w.soft1 !== 0) {
|
|
2902
|
+
// Circle: softness rides SoftRef on the ct4 transform (live: Soft1 67 → 1072).
|
|
2903
|
+
paramEntries.push(createParameterEntry(POWER_WINDOWS.SOFT_REF, w.soft1 * 16));
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
if (paramEntries.length > 0 || groups.softMask.length > 0) {
|
|
2907
|
+
debugLog('[DRX] Window params built:', paramEntries.length, '+', groups.softMask.length, isLinear ? '(linear: ct4 + ct3 mask)' : '(circle: ct4)');
|
|
2908
|
+
}
|
|
2909
|
+
return groups;
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
/**
|
|
2913
|
+
* Check if any secondary correction tools are used (qualifier, window, HDR wheels)
|
|
2914
|
+
* Used to determine if we need to add those corrector blocks
|
|
2915
|
+
*
|
|
2916
|
+
* @param {Object} colorParams - Full color parameters object
|
|
2917
|
+
* @returns {Object} - { hasQualifier, hasWindow, hasHDRWheels }
|
|
2918
|
+
*/
|
|
2919
|
+
function hasSecondaryCorrections(colorParams) {
|
|
2920
|
+
const hasQualifier = colorParams.qualifier &&
|
|
2921
|
+
(colorParams.qualifier.hueCenter !== undefined ||
|
|
2922
|
+
colorParams.qualifier.satLow !== undefined ||
|
|
2923
|
+
colorParams.qualifier.lumLow !== undefined);
|
|
2924
|
+
|
|
2925
|
+
const hasWindow = colorParams.window &&
|
|
2926
|
+
colorParams.window.type !== undefined &&
|
|
2927
|
+
colorParams.window.type !== 0;
|
|
2928
|
+
|
|
2929
|
+
const hasHDRWheels = colorParams.hdrDark || colorParams.hdrShadow ||
|
|
2930
|
+
colorParams.hdrLight || colorParams.hdrHighlight || colorParams.hdrGlobal ||
|
|
2931
|
+
colorParams.hdrSpecular;
|
|
2932
|
+
|
|
2933
|
+
return { hasQualifier, hasWindow, hasHDRWheels };
|
|
2934
|
+
}
|
|
2935
|
+
|
|
2936
|
+
/**
|
|
2937
|
+
* Check if per-channel gain parameters are specified
|
|
2938
|
+
*/
|
|
2939
|
+
function hasPerChannelGain(adjustments) {
|
|
2940
|
+
return adjustments.gainR !== undefined ||
|
|
2941
|
+
adjustments.gainG !== undefined ||
|
|
2942
|
+
adjustments.gainB !== undefined ||
|
|
2943
|
+
adjustments.gainMaster !== undefined;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
/**
|
|
2947
|
+
* PNT1: Create parallel node structure for skin protection workflow
|
|
2948
|
+
* Creates: Input -> Splitter -> [Node A, Node B] -> Combiner -> Output
|
|
2949
|
+
*
|
|
2950
|
+
* Parallel nodes allow processing the same input through multiple paths
|
|
2951
|
+
* that are then combined. Common use cases:
|
|
2952
|
+
* - Skin protection: Keep skin tones on path A, apply heavy grade on path B
|
|
2953
|
+
* - Selective processing: Different treatments for shadows vs highlights
|
|
2954
|
+
* - Creative blending: Mix multiple looks together
|
|
2955
|
+
*
|
|
2956
|
+
* @param {Object} options - Configuration options
|
|
2957
|
+
* @param {Object} options.nodeA - First parallel node configuration
|
|
2958
|
+
* @param {Object} options.nodeA.params - Color parameters for node A
|
|
2959
|
+
* @param {string} options.nodeA.label - Label for node A (default: 'Path A')
|
|
2960
|
+
* @param {Object} options.nodeB - Second parallel node configuration
|
|
2961
|
+
* @param {Object} options.nodeB.params - Color parameters for node B
|
|
2962
|
+
* @param {string} options.nodeB.label - Label for node B (default: 'Path B')
|
|
2963
|
+
* @param {number} options.mixRatio - Mix ratio between paths (0=all A, 1=all B, 0.5=equal, default: 0.5)
|
|
2964
|
+
* @param {string} options.combineMode - How to combine paths ('mix' | 'add' | 'multiply', default: 'mix')
|
|
2965
|
+
* @returns {Object} - Parallel node graph structure with nodes and connections
|
|
2966
|
+
*/
|
|
2967
|
+
function createParallelNodes(options = {}) {
|
|
2968
|
+
const {
|
|
2969
|
+
nodeA = { params: null, label: 'Path A' },
|
|
2970
|
+
nodeB = { params: null, label: 'Path B' },
|
|
2971
|
+
mixRatio = 0.5,
|
|
2972
|
+
combineMode = 'mix',
|
|
2973
|
+
} = options;
|
|
2974
|
+
|
|
2975
|
+
// Node IDs in the parallel structure (these will be offset by baseNodeId in generateMultiNodeDRX)
|
|
2976
|
+
// Structure: Splitter (1) -> [Node A (2), Node B (3)] -> Combiner (4)
|
|
2977
|
+
const nodes = [
|
|
2978
|
+
{
|
|
2979
|
+
id: 1,
|
|
2980
|
+
label: 'Splitter',
|
|
2981
|
+
type: 'splitter',
|
|
2982
|
+
params: null, // Splitter has no color params
|
|
2983
|
+
xPos: 100,
|
|
2984
|
+
yPos: 180,
|
|
2985
|
+
},
|
|
2986
|
+
{
|
|
2987
|
+
id: 2,
|
|
2988
|
+
label: nodeA.label || 'Path A',
|
|
2989
|
+
type: 'corrector',
|
|
2990
|
+
params: nodeA.params || null,
|
|
2991
|
+
xPos: 370,
|
|
2992
|
+
yPos: 100, // Upper path
|
|
2993
|
+
},
|
|
2994
|
+
{
|
|
2995
|
+
id: 3,
|
|
2996
|
+
label: nodeB.label || 'Path B',
|
|
2997
|
+
type: 'corrector',
|
|
2998
|
+
params: nodeB.params || null,
|
|
2999
|
+
xPos: 370,
|
|
3000
|
+
yPos: 260, // Lower path
|
|
3001
|
+
},
|
|
3002
|
+
{
|
|
3003
|
+
id: 4,
|
|
3004
|
+
label: 'Combiner',
|
|
3005
|
+
type: 'combiner',
|
|
3006
|
+
params: null, // Combiner has no color params
|
|
3007
|
+
mixRatio,
|
|
3008
|
+
combineMode,
|
|
3009
|
+
xPos: 640,
|
|
3010
|
+
yPos: 180,
|
|
3011
|
+
},
|
|
3012
|
+
];
|
|
3013
|
+
|
|
3014
|
+
// Connections for parallel structure
|
|
3015
|
+
// Splitter outputs to both paths, both paths feed into combiner
|
|
3016
|
+
const connections = [
|
|
3017
|
+
{ from: 1, to: 2 }, // Splitter -> Path A
|
|
3018
|
+
{ from: 1, to: 3 }, // Splitter -> Path B
|
|
3019
|
+
{ from: 2, to: 4 }, // Path A -> Combiner
|
|
3020
|
+
{ from: 3, to: 4 }, // Path B -> Combiner
|
|
3021
|
+
];
|
|
3022
|
+
|
|
3023
|
+
return {
|
|
3024
|
+
type: 'parallel',
|
|
3025
|
+
nodes,
|
|
3026
|
+
connections,
|
|
3027
|
+
metadata: {
|
|
3028
|
+
mixRatio,
|
|
3029
|
+
combineMode,
|
|
3030
|
+
nodeALabel: nodeA.label,
|
|
3031
|
+
nodeBLabel: nodeB.label,
|
|
3032
|
+
},
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
/**
|
|
3037
|
+
* PNT2: Create layer mixer node for blending
|
|
3038
|
+
* Layer mixers allow blending foreground and background nodes with various blend modes.
|
|
3039
|
+
*
|
|
3040
|
+
* Blend modes:
|
|
3041
|
+
* - normal: Standard opacity blend (foreground over background)
|
|
3042
|
+
* - overlay: Increases contrast, combines multiply and screen
|
|
3043
|
+
* - softLight: Softer version of overlay, good for subtle adjustments
|
|
3044
|
+
* - multiply: Darkens by multiplying colors (black stays black)
|
|
3045
|
+
* - screen: Lightens by inverting, multiplying, inverting again
|
|
3046
|
+
* - add: Additive blending (can blow out highlights)
|
|
3047
|
+
* - subtract: Subtractive blending (can crush blacks)
|
|
3048
|
+
*
|
|
3049
|
+
* @param {string} blendMode - Blend mode ('normal' | 'overlay' | 'softLight' | 'multiply' | 'screen' | 'add' | 'subtract')
|
|
3050
|
+
* @param {number} opacity - Opacity of blend (0-1, where 1 = full effect)
|
|
3051
|
+
* @param {Object} foregroundNode - Foreground node configuration
|
|
3052
|
+
* @param {Object} foregroundNode.params - Color parameters for foreground
|
|
3053
|
+
* @param {string} foregroundNode.label - Label for foreground node
|
|
3054
|
+
* @param {Object} backgroundNode - Background node configuration
|
|
3055
|
+
* @param {Object} backgroundNode.params - Color parameters for background
|
|
3056
|
+
* @param {string} backgroundNode.label - Label for background node
|
|
3057
|
+
* @returns {Object} - Layer mixer node configuration with nodes and connections
|
|
3058
|
+
*/
|
|
3059
|
+
function createLayerMixer(blendMode, opacity, foregroundNode, backgroundNode) {
|
|
3060
|
+
// Validate blend mode
|
|
3061
|
+
const validBlendModes = ['normal', 'overlay', 'softLight', 'multiply', 'screen', 'add', 'subtract'];
|
|
3062
|
+
if (!validBlendModes.includes(blendMode)) {
|
|
3063
|
+
console.warn(`[DRX] Invalid blend mode '${blendMode}', defaulting to 'normal'`);
|
|
3064
|
+
blendMode = 'normal';
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
// Clamp opacity to valid range
|
|
3068
|
+
opacity = Math.max(0, Math.min(1, opacity));
|
|
3069
|
+
|
|
3070
|
+
// Map blend mode to DaVinci Resolve layer mixer type ID
|
|
3071
|
+
// These are approximations based on Resolve's internal blend mode encoding
|
|
3072
|
+
const blendModeIds = {
|
|
3073
|
+
normal: 0,
|
|
3074
|
+
overlay: 1,
|
|
3075
|
+
softLight: 2,
|
|
3076
|
+
multiply: 3,
|
|
3077
|
+
screen: 4,
|
|
3078
|
+
add: 5,
|
|
3079
|
+
subtract: 6,
|
|
3080
|
+
};
|
|
3081
|
+
|
|
3082
|
+
const nodes = [
|
|
3083
|
+
{
|
|
3084
|
+
id: 1,
|
|
3085
|
+
label: backgroundNode?.label || 'Background',
|
|
3086
|
+
type: 'corrector',
|
|
3087
|
+
params: backgroundNode?.params || null,
|
|
3088
|
+
xPos: 100,
|
|
3089
|
+
yPos: 180,
|
|
3090
|
+
},
|
|
3091
|
+
{
|
|
3092
|
+
id: 2,
|
|
3093
|
+
label: foregroundNode?.label || 'Foreground',
|
|
3094
|
+
type: 'corrector',
|
|
3095
|
+
params: foregroundNode?.params || null,
|
|
3096
|
+
xPos: 370,
|
|
3097
|
+
yPos: 100, // Foreground above background in node graph
|
|
3098
|
+
},
|
|
3099
|
+
{
|
|
3100
|
+
id: 3,
|
|
3101
|
+
label: `Layer Mixer (${blendMode})`,
|
|
3102
|
+
type: 'layer_mixer',
|
|
3103
|
+
blendMode,
|
|
3104
|
+
blendModeId: blendModeIds[blendMode],
|
|
3105
|
+
opacity,
|
|
3106
|
+
xPos: 640,
|
|
3107
|
+
yPos: 180,
|
|
3108
|
+
},
|
|
3109
|
+
];
|
|
3110
|
+
|
|
3111
|
+
// Connections: Background and Foreground both feed into Layer Mixer
|
|
3112
|
+
const connections = [
|
|
3113
|
+
{ from: 1, to: 3 }, // Background -> Layer Mixer (as background input)
|
|
3114
|
+
{ from: 2, to: 3 }, // Foreground -> Layer Mixer (as foreground input)
|
|
3115
|
+
];
|
|
3116
|
+
|
|
3117
|
+
return {
|
|
3118
|
+
type: 'layer_mixer',
|
|
3119
|
+
nodes,
|
|
3120
|
+
connections,
|
|
3121
|
+
metadata: {
|
|
3122
|
+
blendMode,
|
|
3123
|
+
blendModeId: blendModeIds[blendMode],
|
|
3124
|
+
opacity,
|
|
3125
|
+
foregroundLabel: foregroundNode?.label || 'Foreground',
|
|
3126
|
+
backgroundLabel: backgroundNode?.label || 'Background',
|
|
3127
|
+
},
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
/**
|
|
3132
|
+
* PNT3: Create outside node (inverse of qualified/windowed area)
|
|
3133
|
+
* Used for: grade everything EXCEPT the qualified area
|
|
3134
|
+
*
|
|
3135
|
+
* Outside nodes are essential for workflows like:
|
|
3136
|
+
* - Sky replacement: Grade the sky, leave the rest untouched
|
|
3137
|
+
* - Subject isolation: Protect skin tones, grade everything else
|
|
3138
|
+
* - Vignette effects: Grade the edges, leave center clean
|
|
3139
|
+
*
|
|
3140
|
+
* The outside node inverts the effect of a qualifier or power window,
|
|
3141
|
+
* applying corrections to the non-selected area.
|
|
3142
|
+
*
|
|
3143
|
+
* @param {Object} qualifierNode - The node containing the qualifier/window
|
|
3144
|
+
* @param {Object} qualifierNode.params - Color parameters of the qualified node
|
|
3145
|
+
* @param {string} qualifierNode.label - Label for the qualified node
|
|
3146
|
+
* @param {Object} qualifierNode.qualifier - HSL qualifier settings (optional)
|
|
3147
|
+
* @param {Object} qualifierNode.window - Power window settings (optional)
|
|
3148
|
+
* @param {Object} outsideParams - Color parameters to apply to the outside (non-qualified) area
|
|
3149
|
+
* @param {Object} outsideParams.params - Color correction parameters for outside
|
|
3150
|
+
* @param {string} outsideParams.label - Label for the outside node
|
|
3151
|
+
* @returns {Object} - Outside node configuration with paired qualifier and outside nodes
|
|
3152
|
+
*/
|
|
3153
|
+
function createOutsideNode(qualifierNode, outsideParams) {
|
|
3154
|
+
// Default configurations
|
|
3155
|
+
const qualifiedLabel = qualifierNode?.label || 'Qualified Area';
|
|
3156
|
+
const outsideLabel = outsideParams?.label || 'Outside (Inverse)';
|
|
3157
|
+
|
|
3158
|
+
const nodes = [
|
|
3159
|
+
{
|
|
3160
|
+
id: 1,
|
|
3161
|
+
label: qualifiedLabel,
|
|
3162
|
+
type: 'corrector',
|
|
3163
|
+
params: qualifierNode?.params || null,
|
|
3164
|
+
qualifier: qualifierNode?.qualifier || null, // HSL qualifier settings
|
|
3165
|
+
window: qualifierNode?.window || null, // Power window settings
|
|
3166
|
+
xPos: 100,
|
|
3167
|
+
yPos: 180,
|
|
3168
|
+
},
|
|
3169
|
+
{
|
|
3170
|
+
id: 2,
|
|
3171
|
+
label: outsideLabel,
|
|
3172
|
+
type: 'outside', // Special type indicating this is an outside node
|
|
3173
|
+
params: outsideParams?.params || null,
|
|
3174
|
+
// The outside node references the qualifier from node 1
|
|
3175
|
+
outsideOf: 1, // Reference to the qualified node
|
|
3176
|
+
invertQualifier: true, // Flag indicating this inverts the qualifier
|
|
3177
|
+
xPos: 370,
|
|
3178
|
+
yPos: 180,
|
|
3179
|
+
},
|
|
3180
|
+
];
|
|
3181
|
+
|
|
3182
|
+
// Serial connection - the outside node follows the qualified node
|
|
3183
|
+
const connections = [
|
|
3184
|
+
{ from: 1, to: 2 }, // Qualified -> Outside
|
|
3185
|
+
];
|
|
3186
|
+
|
|
3187
|
+
return {
|
|
3188
|
+
type: 'outside',
|
|
3189
|
+
nodes,
|
|
3190
|
+
connections,
|
|
3191
|
+
metadata: {
|
|
3192
|
+
qualifiedNodeId: 1,
|
|
3193
|
+
outsideNodeId: 2,
|
|
3194
|
+
qualifiedLabel,
|
|
3195
|
+
outsideLabel,
|
|
3196
|
+
hasQualifier: !!(qualifierNode?.qualifier),
|
|
3197
|
+
hasWindow: !!(qualifierNode?.window),
|
|
3198
|
+
},
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
/**
|
|
3203
|
+
* Helper: Create a neutral node base for professional node structures
|
|
3204
|
+
* @returns {Object} - Neutral grade parameters
|
|
3205
|
+
*/
|
|
3206
|
+
function createNeutralNodeParams() {
|
|
3207
|
+
return {
|
|
3208
|
+
lift: { r: 0, g: 0, b: 0, master: 0 },
|
|
3209
|
+
gamma: { r: 0, g: 0, b: 0, master: 0 },
|
|
3210
|
+
gain: { r: 1, g: 1, b: 1, master: 1 },
|
|
3211
|
+
offset: { r: 0, g: 0, b: 0 },
|
|
3212
|
+
saturation: 1.0,
|
|
3213
|
+
contrast: 1.0,
|
|
3214
|
+
pivot: 0.435,
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
/**
|
|
3219
|
+
* Parse AI adjustments into separate labeled nodes for modular grading
|
|
3220
|
+
* Each adjustment type gets its own node with a descriptive label
|
|
3221
|
+
*
|
|
3222
|
+
* @param {Object} adjustments - AI adjustment parameters
|
|
3223
|
+
* @param {Object} options - Optional settings
|
|
3224
|
+
* @param {boolean} options.forceNeutralIfEmpty - If true, return a neutral "Reset" node when all values are near-neutral
|
|
3225
|
+
* (useful for tweaks that cancel out, e.g., "warm" then "cool")
|
|
3226
|
+
* @returns {Array} - Array of node definitions for multi-node DRX
|
|
3227
|
+
*/
|
|
3228
|
+
function parseAdjustmentsToNodes(adjustments, options = {}) {
|
|
3229
|
+
const { forceNeutralIfEmpty = false } = options;
|
|
3230
|
+
debugLog('[DRX] parseAdjustmentsToNodes input:', JSON.stringify(adjustments, null, 2));
|
|
3231
|
+
debugLog('[DRX] parseAdjustmentsToNodes options:', { forceNeutralIfEmpty });
|
|
3232
|
+
|
|
3233
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3234
|
+
// SEMANTIC RANGE DETECTION — auto-scale values sent in -1/+1 range
|
|
3235
|
+
// LLMs sometimes send semantic values (saturation: 0.6) instead of UI values
|
|
3236
|
+
// (saturation: 60). Detect this and correct it to prevent catastrophic results
|
|
3237
|
+
// like near-zero saturation (B&W image) or silent temperature drops.
|
|
3238
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
3239
|
+
adjustments = { ...adjustments }; // shallow copy to avoid mutating caller's object
|
|
3240
|
+
|
|
3241
|
+
// Saturation: UI range 0-100 (50=neutral). Semantic range -1 to +1.
|
|
3242
|
+
// If value is between -1.5 and +1.5 and not exactly 0, it's almost certainly semantic.
|
|
3243
|
+
if (adjustments.saturation !== undefined && Math.abs(adjustments.saturation) <= 1.5 && adjustments.saturation !== 0) {
|
|
3244
|
+
const original = adjustments.saturation;
|
|
3245
|
+
// Convert: semantic -1→0, 0→50, +1→100
|
|
3246
|
+
adjustments.saturation = 50 + (adjustments.saturation * 50);
|
|
3247
|
+
console.warn(`[DRX] AUTO-SCALED saturation: ${original} (semantic) → ${adjustments.saturation} (UI). Caller should send UI values 0-100 where 50=neutral.`);
|
|
3248
|
+
}
|
|
3249
|
+
|
|
3250
|
+
// Temperature: UI range -4000 to +4000 (0=neutral). Semantic range -1 to +1.
|
|
3251
|
+
// If abs(value) <= 1.5, it's semantic (a real UI temp of 1 is invisible).
|
|
3252
|
+
if (adjustments.temperature !== undefined && Math.abs(adjustments.temperature) <= 1.5 && adjustments.temperature !== 0) {
|
|
3253
|
+
const original = adjustments.temperature;
|
|
3254
|
+
adjustments.temperature = adjustments.temperature * 4000;
|
|
3255
|
+
console.warn(`[DRX] AUTO-SCALED temperature: ${original} (semantic) → ${adjustments.temperature} (UI). Caller should send UI values -4000 to +4000.`);
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
// Tint: UI range -100 to +100 (0=neutral). Semantic range -1 to +1.
|
|
3259
|
+
// If abs(value) <= 1.5, it's semantic.
|
|
3260
|
+
if (adjustments.tint !== undefined && Math.abs(adjustments.tint) <= 1.5 && adjustments.tint !== 0) {
|
|
3261
|
+
const original = adjustments.tint;
|
|
3262
|
+
adjustments.tint = adjustments.tint * 100;
|
|
3263
|
+
console.warn(`[DRX] AUTO-SCALED tint: ${original} (semantic) → ${adjustments.tint} (UI). Caller should send UI values -100 to +100.`);
|
|
3264
|
+
}
|
|
3265
|
+
|
|
3266
|
+
// MidtoneDetail: UI range -100 to +100. Semantic range -1 to +1.
|
|
3267
|
+
if (adjustments.midtoneDetail !== undefined && Math.abs(adjustments.midtoneDetail) <= 1.5 && adjustments.midtoneDetail !== 0) {
|
|
3268
|
+
const original = adjustments.midtoneDetail;
|
|
3269
|
+
adjustments.midtoneDetail = adjustments.midtoneDetail * 100;
|
|
3270
|
+
console.warn(`[DRX] AUTO-SCALED midtoneDetail: ${original} (semantic) → ${adjustments.midtoneDetail} (UI). Caller should send UI values -100 to +100.`);
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
const nodes = [];
|
|
3274
|
+
|
|
3275
|
+
// Helper to create a neutral node base
|
|
3276
|
+
const createNeutralParams = () => ({
|
|
3277
|
+
lift: { r: 0, g: 0, b: 0, master: 0 },
|
|
3278
|
+
gamma: { r: 0, g: 0, b: 0, master: 0 },
|
|
3279
|
+
gain: { r: 1, g: 1, b: 1, master: 1 },
|
|
3280
|
+
offset: { r: 0, g: 0, b: 0 },
|
|
3281
|
+
saturation: 50, // Resolve Primaries: 0-100, unity=50
|
|
3282
|
+
contrast: 1.0, // Resolve Primaries: 0.0-2.0, unity=1.0
|
|
3283
|
+
pivot: 0.435,
|
|
3284
|
+
temperature: 0, // Resolve Primaries: -4000 to +4000, unity=0
|
|
3285
|
+
tint: 0, // Resolve Primaries: -100 to +100, unity=0
|
|
3286
|
+
});
|
|
3287
|
+
|
|
3288
|
+
// Node 1: Exposure (if adjusted)
|
|
3289
|
+
if (adjustments.exposure && Math.abs(adjustments.exposure) > 0.01) {
|
|
3290
|
+
const params = createNeutralParams();
|
|
3291
|
+
params.gain.master = 1 + adjustments.exposure;
|
|
3292
|
+
debugLog('[DRX] Exposure node: input=', adjustments.exposure, 'output gain.master=', params.gain.master);
|
|
3293
|
+
const direction = adjustments.exposure > 0 ? 'Brighter' : 'Darker';
|
|
3294
|
+
nodes.push({
|
|
3295
|
+
label: `Exposure (${direction})`,
|
|
3296
|
+
params,
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
3299
|
+
|
|
3300
|
+
// Node 2: Temperature/Tint (if adjusted)
|
|
3301
|
+
// Input: Resolve UI values — Temperature: -4000 to +4000, Tint: -100 to +100
|
|
3302
|
+
if ((adjustments.temperature && Math.abs(adjustments.temperature) > 1) ||
|
|
3303
|
+
(adjustments.tint && Math.abs(adjustments.tint) > 0.5)) {
|
|
3304
|
+
const params = createNeutralParams();
|
|
3305
|
+
if (adjustments.temperature) {
|
|
3306
|
+
const t = adjustments.temperature;
|
|
3307
|
+
// Pass through as Resolve UI value (already -4000 to +4000)
|
|
3308
|
+
params.temperature = t;
|
|
3309
|
+
// Also apply subtle gain/lift tinting for richer look
|
|
3310
|
+
// Normalize to -1..+1 range for tinting multipliers
|
|
3311
|
+
const tNorm = t / 4000;
|
|
3312
|
+
params.gain.r += tNorm * 0.15;
|
|
3313
|
+
params.gain.b -= tNorm * 0.15;
|
|
3314
|
+
params.lift.r += tNorm * 0.08;
|
|
3315
|
+
params.lift.b -= tNorm * 0.08;
|
|
3316
|
+
debugLog('[DRX] Color Balance node: temp=', t, '(UI value, normalized=', tNorm, ') gain.r=', params.gain.r);
|
|
3317
|
+
}
|
|
3318
|
+
if (adjustments.tint) {
|
|
3319
|
+
const t = adjustments.tint;
|
|
3320
|
+
// Pass through as Resolve UI value (already -100 to +100)
|
|
3321
|
+
params.tint = t;
|
|
3322
|
+
// Normalize to -1..+1 range for tinting multipliers
|
|
3323
|
+
const tNorm = t / 100;
|
|
3324
|
+
params.gain.g -= tNorm * 0.15;
|
|
3325
|
+
params.lift.g -= tNorm * 0.08;
|
|
3326
|
+
}
|
|
3327
|
+
const tempLabel = adjustments.temperature > 0 ? 'Warm' : adjustments.temperature < 0 ? 'Cool' : '';
|
|
3328
|
+
nodes.push({
|
|
3329
|
+
label: `Color Balance${tempLabel ? ` (${tempLabel})` : ''}`,
|
|
3330
|
+
params,
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
|
|
3334
|
+
// Node 2b: Shadow/Highlight Color Split (Teal/Orange effect)
|
|
3335
|
+
// This handles shadowTemperature, highlightTemperature, shadowTint, highlightTint
|
|
3336
|
+
const hasShadowTemp = adjustments.shadowTemperature && Math.abs(adjustments.shadowTemperature) > 0.01;
|
|
3337
|
+
const hasHighlightTemp = adjustments.highlightTemperature && Math.abs(adjustments.highlightTemperature) > 0.01;
|
|
3338
|
+
const hasShadowTint = adjustments.shadowTint && Math.abs(adjustments.shadowTint) > 0.01;
|
|
3339
|
+
const hasHighlightTint = adjustments.highlightTint && Math.abs(adjustments.highlightTint) > 0.01;
|
|
3340
|
+
|
|
3341
|
+
if (hasShadowTemp || hasHighlightTemp || hasShadowTint || hasHighlightTint) {
|
|
3342
|
+
const params = createNeutralParams();
|
|
3343
|
+
|
|
3344
|
+
// Calibrated multipliers for zone-based temperature
|
|
3345
|
+
// These convert normalized (-1 to +1) parameters to Resolve lift/gain values
|
|
3346
|
+
// Calibrated for visible but tasteful color shifts
|
|
3347
|
+
const SHADOW_TEMP_MULTIPLIER = 0.40; // Lift RGB shift per unit temp
|
|
3348
|
+
const HIGHLIGHT_TEMP_MULTIPLIER = 0.35; // Gain RGB shift per unit temp
|
|
3349
|
+
const TINT_MULTIPLIER = 0.30; // Green channel shift for tint
|
|
3350
|
+
|
|
3351
|
+
// Shadow temperature affects lift (shadows)
|
|
3352
|
+
// Negative = teal/cool (more blue, less red)
|
|
3353
|
+
// Positive = warm (more red, less blue)
|
|
3354
|
+
if (hasShadowTemp) {
|
|
3355
|
+
const st = adjustments.shadowTemperature;
|
|
3356
|
+
// Temperature shifts R and B in opposite directions
|
|
3357
|
+
params.lift.r += st * SHADOW_TEMP_MULTIPLIER;
|
|
3358
|
+
params.lift.b -= st * SHADOW_TEMP_MULTIPLIER;
|
|
3359
|
+
// Add green for true teal/cyan (cyan = blue + green)
|
|
3360
|
+
params.lift.g -= st * (SHADOW_TEMP_MULTIPLIER * 0.35);
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
// Highlight temperature affects gain (highlights)
|
|
3364
|
+
// Positive = orange/warm, Negative = cool
|
|
3365
|
+
if (hasHighlightTemp) {
|
|
3366
|
+
const ht = adjustments.highlightTemperature;
|
|
3367
|
+
// Orange = red boost + blue reduction
|
|
3368
|
+
params.gain.r += ht * HIGHLIGHT_TEMP_MULTIPLIER;
|
|
3369
|
+
params.gain.b -= ht * (HIGHLIGHT_TEMP_MULTIPLIER * 0.85);
|
|
3370
|
+
// Slight green for gold/yellow tone
|
|
3371
|
+
params.gain.g += ht * (HIGHLIGHT_TEMP_MULTIPLIER * 0.15);
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
// Shadow tint affects lift green (magenta/green in shadows)
|
|
3375
|
+
if (hasShadowTint) {
|
|
3376
|
+
const stint = adjustments.shadowTint;
|
|
3377
|
+
params.lift.g -= stint * TINT_MULTIPLIER;
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
// Highlight tint affects gain green (magenta/green in highlights)
|
|
3381
|
+
if (hasHighlightTint) {
|
|
3382
|
+
const htint = adjustments.highlightTint;
|
|
3383
|
+
params.gain.g -= htint * TINT_MULTIPLIER;
|
|
3384
|
+
}
|
|
3385
|
+
|
|
3386
|
+
// Create descriptive label
|
|
3387
|
+
let splitLabel = 'Color Split';
|
|
3388
|
+
if (hasShadowTemp && hasHighlightTemp) {
|
|
3389
|
+
const shadowDir = adjustments.shadowTemperature < 0 ? 'Teal' : 'Orange';
|
|
3390
|
+
const highlightDir = adjustments.highlightTemperature > 0 ? 'Orange' : 'Teal';
|
|
3391
|
+
splitLabel = `${shadowDir} Shadows / ${highlightDir} Highlights`;
|
|
3392
|
+
} else if (hasShadowTemp) {
|
|
3393
|
+
splitLabel = adjustments.shadowTemperature < 0 ? 'Teal Shadows' : 'Warm Shadows';
|
|
3394
|
+
} else if (hasHighlightTemp) {
|
|
3395
|
+
splitLabel = adjustments.highlightTemperature > 0 ? 'Orange Highlights' : 'Cool Highlights';
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
nodes.push({
|
|
3399
|
+
label: splitLabel,
|
|
3400
|
+
params,
|
|
3401
|
+
});
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
// Node 3: Contrast (if adjusted)
|
|
3405
|
+
// Input: Resolve UI value — 0 to 2, unity=1.0
|
|
3406
|
+
// NOTE: We implement contrast using Lift/Gain instead of the Contrast Corrector (Type 2)
|
|
3407
|
+
// because Type 2 was causing HSL qualifier issues. Lift/Gain achieves the same effect
|
|
3408
|
+
// and works reliably with the Primary Corrector (Type 1).
|
|
3409
|
+
if (adjustments.contrast !== undefined && Math.abs(adjustments.contrast - 1.0) > 0.01) {
|
|
3410
|
+
const params = createNeutralParams();
|
|
3411
|
+
const c = adjustments.contrast;
|
|
3412
|
+
|
|
3413
|
+
// Implement contrast using Lift (shadows) and Gain (highlights)
|
|
3414
|
+
// Offset from unity (1.0): positive = more contrast, negative = less
|
|
3415
|
+
const contrastAmount = (c - 1.0) * 0.15; // Scale for subtlety
|
|
3416
|
+
|
|
3417
|
+
// Adjust lift (shadows) - lower for more contrast
|
|
3418
|
+
params.lift.r = -contrastAmount;
|
|
3419
|
+
params.lift.g = -contrastAmount;
|
|
3420
|
+
params.lift.b = -contrastAmount;
|
|
3421
|
+
params.lift.master = -contrastAmount * 0.5;
|
|
3422
|
+
|
|
3423
|
+
// Adjust gain (highlights) - raise for more contrast
|
|
3424
|
+
params.gain.r = 1.0 + contrastAmount;
|
|
3425
|
+
params.gain.g = 1.0 + contrastAmount;
|
|
3426
|
+
params.gain.b = 1.0 + contrastAmount;
|
|
3427
|
+
params.gain.master = 1.0 + (contrastAmount * 0.5);
|
|
3428
|
+
|
|
3429
|
+
debugLog('[DRX] Contrast node: input=', c, '(offset from unity=', c - 1.0, ') lift=', params.lift.r, 'gain=', params.gain.r);
|
|
3430
|
+
|
|
3431
|
+
const direction = adjustments.contrast > 1.0 ? 'More' : 'Less';
|
|
3432
|
+
nodes.push({
|
|
3433
|
+
label: `Contrast (${direction})`,
|
|
3434
|
+
params,
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
// Node 4: Shadows (if adjusted)
|
|
3439
|
+
if (adjustments.shadowLift && Math.abs(adjustments.shadowLift) > 0.01) {
|
|
3440
|
+
const params = createNeutralParams();
|
|
3441
|
+
const s = adjustments.shadowLift;
|
|
3442
|
+
// Stronger multiplier for visible shadow changes (0.25 instead of 0.1)
|
|
3443
|
+
params.lift.r += s * 0.25;
|
|
3444
|
+
params.lift.g += s * 0.25;
|
|
3445
|
+
params.lift.b += s * 0.25;
|
|
3446
|
+
params.lift.master += s * 0.15; // Also adjust master for overall effect
|
|
3447
|
+
debugLog('[DRX] Shadows node: input=', s, 'output lift.r=', params.lift.r, 'lift.master=', params.lift.master);
|
|
3448
|
+
const direction = adjustments.shadowLift > 0 ? 'Lifted' : 'Crushed';
|
|
3449
|
+
nodes.push({
|
|
3450
|
+
label: `Shadows (${direction})`,
|
|
3451
|
+
params,
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
// Node 5: Highlights (if adjusted)
|
|
3456
|
+
if (adjustments.highlightCompression && Math.abs(adjustments.highlightCompression) > 0.01) {
|
|
3457
|
+
const params = createNeutralParams();
|
|
3458
|
+
const h = adjustments.highlightCompression;
|
|
3459
|
+
params.gain.r -= h * 0.1;
|
|
3460
|
+
params.gain.g -= h * 0.1;
|
|
3461
|
+
params.gain.b -= h * 0.1;
|
|
3462
|
+
nodes.push({
|
|
3463
|
+
label: 'Highlights (Rolled)',
|
|
3464
|
+
params,
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
// Node 6: Saturation (if adjusted)
|
|
3469
|
+
// Input: Resolve UI value — 0 to 100, unity=50
|
|
3470
|
+
if (adjustments.saturation !== undefined && Math.abs(adjustments.saturation - 50) > 0.5) {
|
|
3471
|
+
const params = createNeutralParams();
|
|
3472
|
+
params.saturation = adjustments.saturation;
|
|
3473
|
+
debugLog('[DRX] Saturation node: input=', adjustments.saturation, '(UI value, unity=50)');
|
|
3474
|
+
const direction = adjustments.saturation > 50 ? 'Vibrant' : 'Muted';
|
|
3475
|
+
nodes.push({
|
|
3476
|
+
label: `Saturation (${direction})`,
|
|
3477
|
+
params,
|
|
3478
|
+
});
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
// Node 7: Midtone Detail (if adjusted)
|
|
3482
|
+
// Input: Resolve UI value — -100 to +100, unity=0
|
|
3483
|
+
// buildPrimaryCorrectorParams now passes through (no ×100 scaling)
|
|
3484
|
+
if (adjustments.midtoneDetail !== undefined && Math.abs(adjustments.midtoneDetail) > 0.5) {
|
|
3485
|
+
const params = createNeutralParams();
|
|
3486
|
+
// Midtone Detail maps to DaVinci's Mid/Detail slider in the Primaries panel
|
|
3487
|
+
// Positive values = sharper midtones, negative = softer midtones
|
|
3488
|
+
// Pass through directly — buildPrimary clamps to [-100, +100]
|
|
3489
|
+
params.midtoneDetail = adjustments.midtoneDetail;
|
|
3490
|
+
debugLog('[DRX] Midtone Detail node: input=', adjustments.midtoneDetail, '(UI value)');
|
|
3491
|
+
const direction = adjustments.midtoneDetail > 0 ? 'Sharper' : 'Softer';
|
|
3492
|
+
nodes.push({
|
|
3493
|
+
label: `Midtone Detail (${direction})`,
|
|
3494
|
+
params,
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
// Node 8: HDR Zones + Black Offset (if adjusted)
|
|
3499
|
+
// Check for hdrDark, hdrShadow, hdrLight, hdrHighlight, hdrGlobal
|
|
3500
|
+
const hdrZones = ['hdrDark', 'hdrShadow', 'hdrLight', 'hdrHighlight', 'hdrGlobal'];
|
|
3501
|
+
const activeHdrZones = hdrZones.filter(zone => {
|
|
3502
|
+
const zoneParams = adjustments[zone];
|
|
3503
|
+
if (!zoneParams) return false;
|
|
3504
|
+
// Check if zone has non-default exposure (non-zero) or saturation (non-1)
|
|
3505
|
+
const hasExposure = zoneParams.exposure !== undefined && Math.abs(zoneParams.exposure) > 0.01;
|
|
3506
|
+
const hasSaturation = zoneParams.saturation !== undefined && Math.abs(zoneParams.saturation - 1) > 0.01;
|
|
3507
|
+
return hasExposure || hasSaturation;
|
|
3508
|
+
});
|
|
3509
|
+
const hasBlackOffset = adjustments.blackOffset !== undefined && Math.abs(adjustments.blackOffset) > 0.001;
|
|
3510
|
+
|
|
3511
|
+
if (activeHdrZones.length > 0 || hasBlackOffset) {
|
|
3512
|
+
const params = createNeutralParams();
|
|
3513
|
+
// Copy HDR zone params to the node params so buildHDRWheelParams can find them
|
|
3514
|
+
for (const zone of activeHdrZones) {
|
|
3515
|
+
params[zone] = adjustments[zone];
|
|
3516
|
+
}
|
|
3517
|
+
// Black Offset is a global HDR control, encoded as flat param in Primary corrector
|
|
3518
|
+
if (hasBlackOffset) {
|
|
3519
|
+
params.blackOffset = adjustments.blackOffset;
|
|
3520
|
+
}
|
|
3521
|
+
// Generate descriptive label
|
|
3522
|
+
const zoneDescriptions = activeHdrZones.map(zone => {
|
|
3523
|
+
const zoneParams = adjustments[zone];
|
|
3524
|
+
const zoneName = zone.replace('hdr', '');
|
|
3525
|
+
const exp = zoneParams.exposure || 0;
|
|
3526
|
+
return `${zoneName} ${exp > 0 ? '+' : ''}${exp.toFixed(2)}`;
|
|
3527
|
+
});
|
|
3528
|
+
if (hasBlackOffset) {
|
|
3529
|
+
zoneDescriptions.push(`BlackOffset ${adjustments.blackOffset > 0 ? '+' : ''}${adjustments.blackOffset.toFixed(3)}`);
|
|
3530
|
+
}
|
|
3531
|
+
debugLog('[DRX] HDR Zones node: zones=', activeHdrZones.join(', '));
|
|
3532
|
+
nodes.push({
|
|
3533
|
+
label: `HDR (${zoneDescriptions.join(', ')})`,
|
|
3534
|
+
params,
|
|
3535
|
+
hasHDRWheels: true, // Flag for createNode to call buildHDRWheelParams
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
// Node 9: LOG Wheels (if adjusted)
|
|
3540
|
+
// Check for logShadowR/G/B, logMidR/G/B, logHighR/G/B
|
|
3541
|
+
const logChannels = ['logShadowR', 'logShadowG', 'logShadowB', 'logMidR', 'logMidG', 'logMidB', 'logHighR', 'logHighG', 'logHighB'];
|
|
3542
|
+
const activeLogChannels = logChannels.filter(ch => {
|
|
3543
|
+
return adjustments[ch] !== undefined && Math.abs(adjustments[ch]) > 0.005;
|
|
3544
|
+
});
|
|
3545
|
+
|
|
3546
|
+
if (activeLogChannels.length > 0) {
|
|
3547
|
+
const params = createNeutralParams();
|
|
3548
|
+
// Convert flat logShadowR/G/B to nested logShadow: {r, g, b} structure for encoding
|
|
3549
|
+
params.logShadow = { r: 0, g: 0, b: 0 };
|
|
3550
|
+
params.logMid = { r: 0, g: 0, b: 0 };
|
|
3551
|
+
params.logHigh = { r: 0, g: 0, b: 0 };
|
|
3552
|
+
|
|
3553
|
+
for (const ch of activeLogChannels) {
|
|
3554
|
+
if (ch.startsWith('logShadow')) {
|
|
3555
|
+
const channel = ch.replace('logShadow', '').toLowerCase();
|
|
3556
|
+
params.logShadow[channel] = adjustments[ch];
|
|
3557
|
+
} else if (ch.startsWith('logMid')) {
|
|
3558
|
+
const channel = ch.replace('logMid', '').toLowerCase();
|
|
3559
|
+
params.logMid[channel] = adjustments[ch];
|
|
3560
|
+
} else if (ch.startsWith('logHigh')) {
|
|
3561
|
+
const channel = ch.replace('logHigh', '').toLowerCase();
|
|
3562
|
+
params.logHigh[channel] = adjustments[ch];
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
debugLog('[DRX] LOG Wheels node: logShadow=', params.logShadow, 'logMid=', params.logMid, 'logHigh=', params.logHigh);
|
|
3566
|
+
nodes.push({
|
|
3567
|
+
label: 'LOG Wheels',
|
|
3568
|
+
params,
|
|
3569
|
+
});
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
// Node 9b: RGB Mixer (if adjusted)
|
|
3573
|
+
if (adjustments.rgbMixer) {
|
|
3574
|
+
const params = createNeutralParams();
|
|
3575
|
+
params.rgbMixer = adjustments.rgbMixer;
|
|
3576
|
+
debugLog('[DRX] RGB Mixer node');
|
|
3577
|
+
nodes.push({
|
|
3578
|
+
label: 'RGB Mixer',
|
|
3579
|
+
params,
|
|
3580
|
+
});
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
// Node 9c: Custom Curves YRGB (if adjusted)
|
|
3584
|
+
if (adjustments.customCurves) {
|
|
3585
|
+
const params = createNeutralParams();
|
|
3586
|
+
params.customCurves = adjustments.customCurves;
|
|
3587
|
+
const channelList = Object.keys(adjustments.customCurves).filter(k => adjustments.customCurves[k]?.length > 0);
|
|
3588
|
+
debugLog('[DRX] Custom Curves node: channels=', channelList.join(','));
|
|
3589
|
+
nodes.push({
|
|
3590
|
+
label: 'Curves (' + channelList.map(c => c.toUpperCase()).join('/') + ')',
|
|
3591
|
+
params,
|
|
3592
|
+
});
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
// Node 9d: HSL Curves (if adjusted)
|
|
3596
|
+
if (adjustments.hslCurves) {
|
|
3597
|
+
const params = createNeutralParams();
|
|
3598
|
+
params.hslCurves = adjustments.hslCurves;
|
|
3599
|
+
const curveList = Object.keys(adjustments.hslCurves).filter(k => adjustments.hslCurves[k]?.length > 0);
|
|
3600
|
+
debugLog('[DRX] HSL Curves node: curves=', curveList.join(','));
|
|
3601
|
+
nodes.push({
|
|
3602
|
+
label: 'HSL Curves',
|
|
3603
|
+
params,
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
// Node 10: Per-channel primaries (liftR/G/B, gammaR/G/B, gainR/G/B)
|
|
3608
|
+
const perChannelKeys = [
|
|
3609
|
+
'liftR', 'liftG', 'liftB', 'liftMaster',
|
|
3610
|
+
'gammaR', 'gammaG', 'gammaB', 'gammaMaster',
|
|
3611
|
+
'gainR', 'gainG', 'gainB', 'gainMaster',
|
|
3612
|
+
];
|
|
3613
|
+
const activePerChannel = perChannelKeys.filter(k => {
|
|
3614
|
+
return adjustments[k] !== undefined && Math.abs(adjustments[k]) > 0.005;
|
|
3615
|
+
});
|
|
3616
|
+
|
|
3617
|
+
if (activePerChannel.length > 0) {
|
|
3618
|
+
const params = createNeutralParams();
|
|
3619
|
+
// Apply per-channel adjustments to the nested structure
|
|
3620
|
+
for (const key of activePerChannel) {
|
|
3621
|
+
if (key.startsWith('lift')) {
|
|
3622
|
+
const ch = key.replace('lift', '').toLowerCase();
|
|
3623
|
+
params.lift[ch] = (params.lift[ch] || 0) + adjustments[key];
|
|
3624
|
+
} else if (key.startsWith('gamma')) {
|
|
3625
|
+
const ch = key.replace('gamma', '').toLowerCase();
|
|
3626
|
+
params.gamma[ch] = (params.gamma[ch] || 0) + adjustments[key];
|
|
3627
|
+
} else if (key.startsWith('gain')) {
|
|
3628
|
+
const ch = key.replace('gain', '').toLowerCase();
|
|
3629
|
+
// Gain defaults to 1, so add to existing
|
|
3630
|
+
params.gain[ch] = (params.gain[ch] || 1) + adjustments[key];
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
debugLog('[DRX] Per-channel primaries node:', activePerChannel.join(', '));
|
|
3634
|
+
nodes.push({
|
|
3635
|
+
label: 'Color Wheels',
|
|
3636
|
+
params,
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
|
|
3640
|
+
// Node 11: ColorBoost (if adjusted)
|
|
3641
|
+
if (adjustments.colorBoost !== undefined && Math.abs(adjustments.colorBoost) > 0.5) {
|
|
3642
|
+
const params = createNeutralParams();
|
|
3643
|
+
params.colorBoost = adjustments.colorBoost;
|
|
3644
|
+
debugLog('[DRX] ColorBoost node: input=', adjustments.colorBoost);
|
|
3645
|
+
nodes.push({
|
|
3646
|
+
label: `Color Boost (${adjustments.colorBoost > 0 ? '+' : ''}${adjustments.colorBoost})`,
|
|
3647
|
+
params,
|
|
3648
|
+
});
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
// Node 12: Pivot/Contrast Range (if adjusted)
|
|
3652
|
+
const hasPivot = adjustments.pivot !== undefined && Math.abs(adjustments.pivot - 0.435) > 0.01;
|
|
3653
|
+
const hasHighRange = adjustments.contrastHighRange !== undefined && Math.abs(adjustments.contrastHighRange - 0.75) > 0.01;
|
|
3654
|
+
const hasLowRange = adjustments.contrastLowRange !== undefined && Math.abs(adjustments.contrastLowRange - 0.25) > 0.01;
|
|
3655
|
+
|
|
3656
|
+
if (hasPivot || hasHighRange || hasLowRange) {
|
|
3657
|
+
const params = createNeutralParams();
|
|
3658
|
+
if (hasPivot) params.pivot = adjustments.pivot;
|
|
3659
|
+
if (hasHighRange) params.contrastHighRange = adjustments.contrastHighRange;
|
|
3660
|
+
if (hasLowRange) params.contrastLowRange = adjustments.contrastLowRange;
|
|
3661
|
+
debugLog('[DRX] Pivot/Range node: pivot=', params.pivot, 'highRange=', params.contrastHighRange, 'lowRange=', params.contrastLowRange);
|
|
3662
|
+
nodes.push({
|
|
3663
|
+
label: 'Contrast Range',
|
|
3664
|
+
params,
|
|
3665
|
+
});
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
// If no adjustments detected
|
|
3669
|
+
if (nodes.length === 0) {
|
|
3670
|
+
if (forceNeutralIfEmpty) {
|
|
3671
|
+
// Caller explicitly wants a neutral grade (e.g., tweak that cancelled out to neutral)
|
|
3672
|
+
// Generate a Reset node that clears all adjustments
|
|
3673
|
+
debugLog('[DRX] No adjustments but forceNeutralIfEmpty=true - generating Reset node');
|
|
3674
|
+
const neutralParams = createNeutralParams();
|
|
3675
|
+
return [{
|
|
3676
|
+
label: 'Reset (Neutral)',
|
|
3677
|
+
params: neutralParams,
|
|
3678
|
+
}];
|
|
3679
|
+
}
|
|
3680
|
+
debugLog('[DRX] No adjustments detected - returning empty nodes to avoid overwriting existing grade');
|
|
3681
|
+
// Previously we returned a "Base Grade" neutral node, but that was overwriting existing grades
|
|
3682
|
+
// Now we return empty so the caller can decide whether to error or proceed
|
|
3683
|
+
return [];
|
|
3684
|
+
}
|
|
3685
|
+
|
|
3686
|
+
debugLog('[DRX] parseAdjustmentsToNodes output: created', nodes.length, 'nodes');
|
|
3687
|
+
nodes.forEach((n, i) => {
|
|
3688
|
+
debugLog(`[DRX] Output node ${i + 1}: "${n.label}"`, 'saturation:', n.params.saturation, 'contrast:', n.params.contrast, 'gain.master:', n.params.gain.master);
|
|
3689
|
+
});
|
|
3690
|
+
return nodes;
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
/**
|
|
3694
|
+
* Generate a delta-only DRX containing ONLY adjustment nodes (no base grade).
|
|
3695
|
+
* Used for storing deltas that can be re-applied to any base.
|
|
3696
|
+
*
|
|
3697
|
+
* This function is part of the DRX Audit Trail System:
|
|
3698
|
+
* - Produces a DRX file that contains only the new adjustments
|
|
3699
|
+
* - Can be merged with any base grade to produce the same result
|
|
3700
|
+
* - Used for debugging and auditing grade changes
|
|
3701
|
+
*
|
|
3702
|
+
* @param {Object} deltaParams - Adjustment parameters (from tweak or LLM)
|
|
3703
|
+
* @param {Object} metadata - Optional metadata for the DRX
|
|
3704
|
+
* @param {string} metadata.label - Label for the delta DRX
|
|
3705
|
+
* @param {boolean} metadata.skipNeutralNodes - Skip nodes that have no effect (default: true)
|
|
3706
|
+
* @returns {Promise<string|null>} - DRX XML content, or null if no adjustments
|
|
3707
|
+
*/
|
|
3708
|
+
async function generateDeltaDRX(deltaParams, metadata = {}) {
|
|
3709
|
+
const {
|
|
3710
|
+
label = 'Delta Grade',
|
|
3711
|
+
skipNeutralNodes = true,
|
|
3712
|
+
width = 1920,
|
|
3713
|
+
height = 1080,
|
|
3714
|
+
} = metadata;
|
|
3715
|
+
|
|
3716
|
+
debugLog('[DRX] generateDeltaDRX input:', JSON.stringify(deltaParams, null, 2));
|
|
3717
|
+
|
|
3718
|
+
// Parse adjustments into nodes
|
|
3719
|
+
const nodes = parseAdjustmentsToNodes(deltaParams, { forceNeutralIfEmpty: false });
|
|
3720
|
+
|
|
3721
|
+
// If no adjustments, return null (caller can decide what to do)
|
|
3722
|
+
if (!nodes || nodes.length === 0) {
|
|
3723
|
+
debugLog('[DRX] generateDeltaDRX: no adjustments - returning null');
|
|
3724
|
+
return null;
|
|
3725
|
+
}
|
|
3726
|
+
|
|
3727
|
+
// Optionally filter out neutral nodes
|
|
3728
|
+
let filteredNodes = nodes;
|
|
3729
|
+
if (skipNeutralNodes) {
|
|
3730
|
+
filteredNodes = nodes.filter(node => {
|
|
3731
|
+
if (!node.params) return false;
|
|
3732
|
+
const p = node.params;
|
|
3733
|
+
|
|
3734
|
+
// Check if any parameter is non-neutral
|
|
3735
|
+
const hasLift = p.lift && (
|
|
3736
|
+
Math.abs(p.lift.r) > 0.001 ||
|
|
3737
|
+
Math.abs(p.lift.g) > 0.001 ||
|
|
3738
|
+
Math.abs(p.lift.b) > 0.001 ||
|
|
3739
|
+
Math.abs(p.lift.master) > 0.001
|
|
3740
|
+
);
|
|
3741
|
+
const hasGamma = p.gamma && (
|
|
3742
|
+
Math.abs(p.gamma.r) > 0.001 ||
|
|
3743
|
+
Math.abs(p.gamma.g) > 0.001 ||
|
|
3744
|
+
Math.abs(p.gamma.b) > 0.001 ||
|
|
3745
|
+
Math.abs(p.gamma.master) > 0.001
|
|
3746
|
+
);
|
|
3747
|
+
const hasGain = p.gain && (
|
|
3748
|
+
Math.abs(p.gain.r - 1) > 0.001 ||
|
|
3749
|
+
Math.abs(p.gain.g - 1) > 0.001 ||
|
|
3750
|
+
Math.abs(p.gain.b - 1) > 0.001 ||
|
|
3751
|
+
Math.abs(p.gain.master - 1) > 0.001
|
|
3752
|
+
);
|
|
3753
|
+
const hasOffset = p.offset && (
|
|
3754
|
+
Math.abs(p.offset.r) > 0.001 ||
|
|
3755
|
+
Math.abs(p.offset.g) > 0.001 ||
|
|
3756
|
+
Math.abs(p.offset.b) > 0.001
|
|
3757
|
+
);
|
|
3758
|
+
const hasSaturation = p.saturation !== undefined && Math.abs(p.saturation - 50) > 0.001;
|
|
3759
|
+
const hasContrast = p.contrast !== undefined && Math.abs(p.contrast - 1.0) > 0.001;
|
|
3760
|
+
const hasMidtoneDetail = p.midtoneDetail !== undefined && Math.abs(p.midtoneDetail) > 0.001;
|
|
3761
|
+
|
|
3762
|
+
return hasLift || hasGamma || hasGain || hasOffset || hasSaturation || hasContrast || hasMidtoneDetail;
|
|
3763
|
+
});
|
|
3764
|
+
|
|
3765
|
+
if (filteredNodes.length === 0) {
|
|
3766
|
+
debugLog('[DRX] generateDeltaDRX: all nodes filtered as neutral - returning null');
|
|
3767
|
+
return null;
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3771
|
+
debugLog('[DRX] generateDeltaDRX: generating DRX with', filteredNodes.length, 'delta nodes');
|
|
3772
|
+
|
|
3773
|
+
// Create connections for serial node chain
|
|
3774
|
+
const connections = [];
|
|
3775
|
+
for (let i = 0; i < filteredNodes.length - 1; i++) {
|
|
3776
|
+
connections.push({ from: i + 1, to: i + 2 });
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
// Generate the delta DRX using multi-node generator
|
|
3780
|
+
const drxContent = await generateMultiNodeDRX(filteredNodes, connections, {
|
|
3781
|
+
label: `[DELTA] ${label}`,
|
|
3782
|
+
width,
|
|
3783
|
+
height,
|
|
3784
|
+
isDelta: true, // Flag for identification
|
|
3785
|
+
});
|
|
3786
|
+
|
|
3787
|
+
return drxContent;
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
/**
|
|
3791
|
+
* Compute SHA-256 hash of DRX content for sync verification
|
|
3792
|
+
*
|
|
3793
|
+
* @param {string} drxContent - DRX XML content
|
|
3794
|
+
* @returns {string} - SHA-256 hash (first 16 characters)
|
|
3795
|
+
*/
|
|
3796
|
+
function computeDrxHash(drxContent) {
|
|
3797
|
+
if (!drxContent) return null;
|
|
3798
|
+
const crypto = require('crypto');
|
|
3799
|
+
return crypto
|
|
3800
|
+
.createHash('sha256')
|
|
3801
|
+
.update(drxContent)
|
|
3802
|
+
.digest('hex')
|
|
3803
|
+
.slice(0, 16);
|
|
3804
|
+
}
|
|
3805
|
+
|
|
3806
|
+
/**
|
|
3807
|
+
* Validate a generated DRX body hex string
|
|
3808
|
+
*
|
|
3809
|
+
* Decompresses (0x81 + zstd) and parses the protobuf to verify that
|
|
3810
|
+
* node(s) contain corrector blocks (F9) with parameter entries (F3).
|
|
3811
|
+
* Run this after every DRX generation to catch encoding failures before
|
|
3812
|
+
* applying broken DRXs to Resolve.
|
|
3813
|
+
*
|
|
3814
|
+
* @param {string} bodyHex - The hex-encoded DRX body (0x81 + zstd data)
|
|
3815
|
+
* @returns {Promise<{valid: boolean, summary: string, nodes: Array}>}
|
|
3816
|
+
*/
|
|
3817
|
+
async function validateDRXBody(bodyHex) {
|
|
3818
|
+
const { parseDRXBody } = require('./drx-parser');
|
|
3819
|
+
|
|
3820
|
+
try {
|
|
3821
|
+
const result = await parseDRXBody(bodyHex);
|
|
3822
|
+
const { nodes } = result;
|
|
3823
|
+
|
|
3824
|
+
if (!nodes || nodes.length === 0) {
|
|
3825
|
+
return { valid: false, summary: 'No nodes found in DRX body', nodes: [] };
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
const nodeDetails = [];
|
|
3829
|
+
let totalParams = 0;
|
|
3830
|
+
let nodesWithCorrectors = 0;
|
|
3831
|
+
|
|
3832
|
+
for (const node of nodes) {
|
|
3833
|
+
const detail = {
|
|
3834
|
+
id: node.id,
|
|
3835
|
+
label: node.label || '(unlabeled)',
|
|
3836
|
+
hasCorrectors: false,
|
|
3837
|
+
paramCount: 0,
|
|
3838
|
+
params: [],
|
|
3839
|
+
};
|
|
3840
|
+
|
|
3841
|
+
// Check if the node has any non-default parameters
|
|
3842
|
+
if (node.params) {
|
|
3843
|
+
// Count non-default primary params
|
|
3844
|
+
const checks = [
|
|
3845
|
+
{ name: 'lift.r', val: node.params.lift?.r, def: 0 },
|
|
3846
|
+
{ name: 'lift.g', val: node.params.lift?.g, def: 0 },
|
|
3847
|
+
{ name: 'lift.b', val: node.params.lift?.b, def: 0 },
|
|
3848
|
+
{ name: 'lift.master', val: node.params.lift?.master, def: 0 },
|
|
3849
|
+
{ name: 'gamma.r', val: node.params.gamma?.r, def: 0 },
|
|
3850
|
+
{ name: 'gamma.g', val: node.params.gamma?.g, def: 0 },
|
|
3851
|
+
{ name: 'gamma.b', val: node.params.gamma?.b, def: 0 },
|
|
3852
|
+
{ name: 'gamma.master', val: node.params.gamma?.master, def: 0 },
|
|
3853
|
+
{ name: 'gain.r', val: node.params.gain?.r, def: 1 },
|
|
3854
|
+
{ name: 'gain.g', val: node.params.gain?.g, def: 1 },
|
|
3855
|
+
{ name: 'gain.b', val: node.params.gain?.b, def: 1 },
|
|
3856
|
+
{ name: 'gain.master', val: node.params.gain?.master, def: 1 },
|
|
3857
|
+
{ name: 'offset.r', val: node.params.offset?.r, def: 0 },
|
|
3858
|
+
{ name: 'offset.g', val: node.params.offset?.g, def: 0 },
|
|
3859
|
+
{ name: 'offset.b', val: node.params.offset?.b, def: 0 },
|
|
3860
|
+
{ name: 'saturation', val: node.params.saturation, def: 50 },
|
|
3861
|
+
{ name: 'temperature', val: node.params.temperature, def: 0 },
|
|
3862
|
+
{ name: 'tint', val: node.params.tint, def: 0 },
|
|
3863
|
+
{ name: 'contrast', val: node.params.contrast, def: 1 },
|
|
3864
|
+
];
|
|
3865
|
+
|
|
3866
|
+
for (const { name, val, def } of checks) {
|
|
3867
|
+
if (val != null && Math.abs(val - def) > 0.001) {
|
|
3868
|
+
detail.params.push(`${name}=${val}`);
|
|
3869
|
+
detail.paramCount++;
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
|
|
3873
|
+
// Check for NaN values (the bug we're fixing)
|
|
3874
|
+
for (const { name, val } of checks) {
|
|
3875
|
+
if (val != null && isNaN(val)) {
|
|
3876
|
+
return {
|
|
3877
|
+
valid: false,
|
|
3878
|
+
summary: `NaN detected in parameter ${name} on node "${detail.label}" — undefined value leaked into protobuf`,
|
|
3879
|
+
nodes: nodeDetails,
|
|
3880
|
+
};
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
|
|
3885
|
+
// Check raw corrector data from the parser
|
|
3886
|
+
if (node.correctors && node.correctors.length > 0) {
|
|
3887
|
+
detail.hasCorrectors = true;
|
|
3888
|
+
nodesWithCorrectors++;
|
|
3889
|
+
} else if (detail.paramCount > 0) {
|
|
3890
|
+
detail.hasCorrectors = true;
|
|
3891
|
+
nodesWithCorrectors++;
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
totalParams += detail.paramCount;
|
|
3895
|
+
nodeDetails.push(detail);
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
const summary = `${nodes.length} node(s), ${nodesWithCorrectors} with correctors, ${totalParams} non-default param(s): ${nodeDetails.map(n => `[${n.label}: ${n.params.join(', ') || 'neutral'}]`).join(' ')}`;
|
|
3899
|
+
|
|
3900
|
+
return {
|
|
3901
|
+
valid: totalParams > 0 || nodesWithCorrectors > 0,
|
|
3902
|
+
summary,
|
|
3903
|
+
nodes: nodeDetails,
|
|
3904
|
+
};
|
|
3905
|
+
} catch (err) {
|
|
3906
|
+
return {
|
|
3907
|
+
valid: false,
|
|
3908
|
+
summary: `DRX parse failed: ${err.message}`,
|
|
3909
|
+
nodes: [],
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
// ── OFX / ResolveFX generic encoder ─────────────────────────────────────────
|
|
3915
|
+
|
|
3916
|
+
/**
|
|
3917
|
+
* Build a ResolveFX OFX tool entry for inclusion in F7.F10 (node tool list).
|
|
3918
|
+
*
|
|
3919
|
+
* TRAINED 2026-03-22: OFX container format fully decoded from Film Grain DRX.
|
|
3920
|
+
* Generic for ANY ResolveFX plugin — just pass the plugin ID and param name/value pairs.
|
|
3921
|
+
*
|
|
3922
|
+
* OFX container structure:
|
|
3923
|
+
* F7.F10.F1 (tool entry):
|
|
3924
|
+
* F2.F21 (OFX container):
|
|
3925
|
+
* F1 = 0x4F4659 ("OFY" marker)
|
|
3926
|
+
* F2 = plugin ID string (e.g., "com.blackmagicdesign.resolvefx.filmgrain")
|
|
3927
|
+
* F3 = instance ID string
|
|
3928
|
+
* F4 = 1 (enabled)
|
|
3929
|
+
* F5[] (repeated) = param entries:
|
|
3930
|
+
* F1 = param name (string)
|
|
3931
|
+
* F2.F2 = param value (double) OR F2.F5 = param value (string)
|
|
3932
|
+
*
|
|
3933
|
+
* @param {string} pluginId — OFX plugin identifier (e.g., "com.blackmagicdesign.resolvefx.filmgrain")
|
|
3934
|
+
* @param {Object} params — Map of param name → value (number or string)
|
|
3935
|
+
* @param {Object} [options]
|
|
3936
|
+
* @param {string} [options.instanceId] — custom instance ID (auto-generated if omitted)
|
|
3937
|
+
* @param {string} [options.version='3.2'] — ResolveFX version
|
|
3938
|
+
* @returns {Object} — OFX plugin definition for inclusion in DRX adjustments
|
|
3939
|
+
*/
|
|
3940
|
+
function buildResolveFXParams(pluginId, params, options = {}) {
|
|
3941
|
+
const version = options.version || '3.2';
|
|
3942
|
+
const instanceId = options.instanceId || `OfxImageEffectContext_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
3943
|
+
|
|
3944
|
+
return {
|
|
3945
|
+
_ofxPlugin: true,
|
|
3946
|
+
pluginId,
|
|
3947
|
+
instanceId,
|
|
3948
|
+
version,
|
|
3949
|
+
params: { ...params, resolvefxVersion: version },
|
|
3950
|
+
};
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
/**
|
|
3954
|
+
* Encode an OFX plugin tool entry as protobuf bytes for F7.F10.
|
|
3955
|
+
*
|
|
3956
|
+
* Generates the complete OFX container structure:
|
|
3957
|
+
* F10.F1[0] = { F1=0xC0000001, F2={F2=2} } (standard node marker)
|
|
3958
|
+
* F10.F1[1] = { F1=toolId, F2.F5=pluginId } (plugin identifier)
|
|
3959
|
+
* F10.F1[2] = { F1=toolId+3, F2.F5=instanceId } (instance)
|
|
3960
|
+
* F10.F1[3] = { F1=toolId+6, F2.F4=enabled } (enable flag)
|
|
3961
|
+
* F10.F1[4] = { F1=toolId+42, F2.F21=OFXContainer } (params)
|
|
3962
|
+
* F10.F1[5] = { F1=toolId+63, F2.F4=0 } (end marker)
|
|
3963
|
+
*
|
|
3964
|
+
* @param {string} pluginId — e.g., "com.blackmagicdesign.resolvefx.filmgrain"
|
|
3965
|
+
* @param {Object} params — name→value pairs (number or string)
|
|
3966
|
+
* @param {Object} [options]
|
|
3967
|
+
* @returns {Buffer} — F10 field bytes (length-delimited)
|
|
3968
|
+
*/
|
|
3969
|
+
function buildOFXToolEntry(pluginId, params, options = {}) {
|
|
3970
|
+
// Entry ids are UNIVERSAL CONSTANTS across every plugin (verified 2026-07-03 by
|
|
3971
|
+
// byte-diffing native tool lists for filmgrain/CST/acestransform/deflicker/NeatVideo:
|
|
3972
|
+
// all use marker 0xC0000001 · pluginId 0xC0000049 · instanceId 0xC000005E ·
|
|
3973
|
+
// enable 0xC0000063 · OFX container 0xC0000087 · end 0xC00000D2). The old
|
|
3974
|
+
// "Film Grain toolId 0xC0000087 + offsets" scheme misplaced every entry — Resolve
|
|
3975
|
+
// hard-CRASHED deserializing a string where it expected the container. Resolve keys
|
|
3976
|
+
// plugins by the pluginId STRING; there is no per-plugin tool id.
|
|
3977
|
+
const ID_PLUGIN = 0xC0000049, ID_INSTANCE = 0xC000005E, ID_ENABLE = 0xC0000063,
|
|
3978
|
+
ID_CONTAINER = 0xC0000087, ID_END = 0xC00000D2;
|
|
3979
|
+
// F3 is the OFX CONTEXT name, not a unique instance id — native captures carry the
|
|
3980
|
+
// standardized "OfxImageEffectContextFilter" (a synthesized unique string leaves the
|
|
3981
|
+
// plugin un-instantiated: node applies but the effect never engages — found live
|
|
3982
|
+
// 2026-07-03). Callers can still override for generator/transition contexts.
|
|
3983
|
+
const instanceId = options.instanceId || 'OfxImageEffectContextFilter';
|
|
3984
|
+
|
|
3985
|
+
// Build F5 repeated param entries. Native containers always carry resolvefxVersion
|
|
3986
|
+
// and serialize params in name order — mirror both.
|
|
3987
|
+
const withVersion = { resolvefxVersion: options.version || '3.2', ...params };
|
|
3988
|
+
const paramEntries = [];
|
|
3989
|
+
for (const name of Object.keys(withVersion).sort()) {
|
|
3990
|
+
const value = withVersion[name];
|
|
3991
|
+
const nameBuf = Buffer.from(name, 'utf-8');
|
|
3992
|
+
let valueBuf;
|
|
3993
|
+
if (typeof value === 'string') {
|
|
3994
|
+
const strBuf = Buffer.from(value, 'utf-8');
|
|
3995
|
+
valueBuf = protoBytes(5, strBuf);
|
|
3996
|
+
} else {
|
|
3997
|
+
valueBuf = protoFloat64(2, value);
|
|
3998
|
+
}
|
|
3999
|
+
paramEntries.push(protoBytes(5, Buffer.concat([
|
|
4000
|
+
protoBytes(1, nameBuf),
|
|
4001
|
+
protoBytes(2, valueBuf),
|
|
4002
|
+
])));
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
// Build F2.F21 OFX container
|
|
4006
|
+
const ofxContainer = protoBytes(21, Buffer.concat([
|
|
4007
|
+
protoVarint(1, 0x4F4659), // "OFY" marker
|
|
4008
|
+
protoBytes(2, Buffer.from(pluginId, 'utf-8')),
|
|
4009
|
+
protoBytes(3, Buffer.from(instanceId, 'utf-8')),
|
|
4010
|
+
protoVarint(4, 1), // enabled
|
|
4011
|
+
...paramEntries,
|
|
4012
|
+
]));
|
|
4013
|
+
|
|
4014
|
+
// Build tool list entries
|
|
4015
|
+
const entries = [
|
|
4016
|
+
// Standard node marker
|
|
4017
|
+
protoBytes(1, Buffer.concat([
|
|
4018
|
+
protoVarint(1, 0xC0000001),
|
|
4019
|
+
protoBytes(2, protoVarint(2, 2)),
|
|
4020
|
+
])),
|
|
4021
|
+
// Plugin ID
|
|
4022
|
+
protoBytes(1, Buffer.concat([
|
|
4023
|
+
protoVarint(1, ID_PLUGIN),
|
|
4024
|
+
protoBytes(2, protoBytes(5, Buffer.from(pluginId, 'utf-8'))),
|
|
4025
|
+
])),
|
|
4026
|
+
// Instance ID
|
|
4027
|
+
protoBytes(1, Buffer.concat([
|
|
4028
|
+
protoVarint(1, ID_INSTANCE),
|
|
4029
|
+
protoBytes(2, protoBytes(5, Buffer.from(instanceId, 'utf-8'))),
|
|
4030
|
+
])),
|
|
4031
|
+
// Enable flag (native captures carry 0 here for enabled plugins)
|
|
4032
|
+
protoBytes(1, Buffer.concat([
|
|
4033
|
+
protoVarint(1, ID_ENABLE),
|
|
4034
|
+
protoBytes(2, protoVarint(4, 0)),
|
|
4035
|
+
])),
|
|
4036
|
+
// OFX container with params
|
|
4037
|
+
protoBytes(1, Buffer.concat([
|
|
4038
|
+
protoVarint(1, ID_CONTAINER),
|
|
4039
|
+
protoBytes(2, ofxContainer),
|
|
4040
|
+
])),
|
|
4041
|
+
// End marker
|
|
4042
|
+
protoBytes(1, Buffer.concat([
|
|
4043
|
+
protoVarint(1, ID_END),
|
|
4044
|
+
protoBytes(2, protoVarint(4, 0)),
|
|
4045
|
+
])),
|
|
4046
|
+
];
|
|
4047
|
+
|
|
4048
|
+
return protoBytes(10, Buffer.concat(entries));
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
/**
|
|
4052
|
+
* Known ResolveFX plugin IDs and their parameter names.
|
|
4053
|
+
* Populated from OFX calibration autoresearch data.
|
|
4054
|
+
*/
|
|
4055
|
+
const RESOLVEFX_PLUGINS = {
|
|
4056
|
+
filmGrain: {
|
|
4057
|
+
pluginId: 'com.blackmagicdesign.resolvefx.filmgrain',
|
|
4058
|
+
params: {
|
|
4059
|
+
grainMean: 'GrainMean', // 0.0-1.0, default ~0.482
|
|
4060
|
+
grainSize: 'GrainSize', // 0.0-1.0, default ~0.5
|
|
4061
|
+
grainSkew: 'GrainSkew', // 0.0-1.0, default 0.5
|
|
4062
|
+
grainStrength: 'GrainStrength', // 0.0-1.0, default ~0.149
|
|
4063
|
+
textureness: 'Textureness', // 0.0-1.0, default ~0.704
|
|
4064
|
+
saturation: 'saturation', // 0.0-1.0, default 0.0
|
|
4065
|
+
softness: 'softness', // 0.0-1.0, default ~0.298
|
|
4066
|
+
},
|
|
4067
|
+
},
|
|
4068
|
+
// Add more plugins as they're calibrated
|
|
4069
|
+
};
|
|
4070
|
+
|
|
4071
|
+
// CR3: Import CDL exporter for re-export
|
|
4072
|
+
const cdlExporter = require('./cdl-exporter');
|
|
4073
|
+
|
|
4074
|
+
module.exports = {
|
|
4075
|
+
generateDRX,
|
|
4076
|
+
generateDRXBody,
|
|
4077
|
+
generateProtobuf,
|
|
4078
|
+
generateMultiNodeDRX,
|
|
4079
|
+
// DRX Audit Trail System
|
|
4080
|
+
generateDeltaDRX,
|
|
4081
|
+
computeDrxHash,
|
|
4082
|
+
parseAdjustments,
|
|
4083
|
+
parseAdjustmentsToNodes,
|
|
4084
|
+
createNode,
|
|
4085
|
+
createConnection,
|
|
4086
|
+
createResolution,
|
|
4087
|
+
buildPrimaryCorrectorParams,
|
|
4088
|
+
buildContrastCorrectorParams,
|
|
4089
|
+
buildCorrectorBlock,
|
|
4090
|
+
// PNT1: Parallel node support
|
|
4091
|
+
createParallelNodes,
|
|
4092
|
+
// PNT2: Layer mixer support
|
|
4093
|
+
createLayerMixer,
|
|
4094
|
+
// PNT3: Outside node support
|
|
4095
|
+
createOutsideNode,
|
|
4096
|
+
// Helper for professional node structures
|
|
4097
|
+
createNeutralNodeParams,
|
|
4098
|
+
// CR3: CDL export functionality
|
|
4099
|
+
...cdlExporter,
|
|
4100
|
+
// Stub encoding functions (awaiting DRX training)
|
|
4101
|
+
buildHDRWheelParams,
|
|
4102
|
+
buildHueCorrectorParams,
|
|
4103
|
+
buildLumMixCorrectorParams,
|
|
4104
|
+
buildSatVsSatCorrectorParams,
|
|
4105
|
+
buildQualifierParams,
|
|
4106
|
+
buildRGBQualifierParams,
|
|
4107
|
+
buildLumaQualifierParams,
|
|
4108
|
+
buildWindowParams,
|
|
4109
|
+
buildCustomCurveParams,
|
|
4110
|
+
buildHSLCurveParams,
|
|
4111
|
+
hasSecondaryCorrections,
|
|
4112
|
+
isParamIdAvailable,
|
|
4113
|
+
// OFX / ResolveFX
|
|
4114
|
+
buildResolveFXParams,
|
|
4115
|
+
buildOFXToolEntry,
|
|
4116
|
+
RESOLVEFX_PLUGINS,
|
|
4117
|
+
// Validation
|
|
4118
|
+
validateDRXBody,
|
|
4119
|
+
// Constants
|
|
4120
|
+
PARAM_IDS,
|
|
4121
|
+
CORRECTOR_TYPES,
|
|
4122
|
+
NEUTRAL_GRADE,
|
|
4123
|
+
};
|