* @brief Painter dry-run: verify the script compiles and exposes paint(). */
| 6139 | * @brief Painter dry-run: verify the script compiles and exposes paint(). |
| 6140 | */ |
| 6141 | API::CommandResponse API::Handlers::ProjectHandler::painterDryRun(const QString& id, |
| 6142 | const QJsonObject& params) |
| 6143 | { |
| 6144 | if (!params.contains(QStringLiteral("code"))) |
| 6145 | return CommandResponse::makeError( |
| 6146 | id, ErrorCode::MissingParam, QStringLiteral("Missing required parameter: code")); |
| 6147 | |
| 6148 | const auto code = params.value(QStringLiteral("code")).toString(); |
| 6149 | |
| 6150 | QJSEngine engine; |
| 6151 | engine.installExtensions(QJSEngine::ConsoleExtension | QJSEngine::GarbageCollectionExtension); |
| 6152 | |
| 6153 | auto stub = engine.evaluate( |
| 6154 | QStringLiteral("var datasets = []; datasets.length = 0;" |
| 6155 | "var group = { id: 0, title: '', columns: 0, sourceId: 0 };" |
| 6156 | "var frame = { number: 0, timestampMs: 0 };" |
| 6157 | "var theme = new Proxy({}, { get: function() { return '#000000'; } });" |
| 6158 | "function tableGet() { return 0; }" |
| 6159 | "function tableSet() {}" |
| 6160 | "function datasetGetRaw() { return 0; }" |
| 6161 | "function datasetGetFinal() { return 0; }")); |
| 6162 | if (stub.isError()) |
| 6163 | return CommandResponse::makeError(id, |
| 6164 | ErrorCode::ExecutionError, |
| 6165 | QStringLiteral("Painter dry-run bootstrap failed: %1") |
| 6166 | .arg(stub.property(QStringLiteral("message")).toString())); |
| 6167 | |
| 6168 | const auto compiled = engine.evaluate(code, QStringLiteral("painter_dryrun.js")); |
| 6169 | if (compiled.isError()) { |
| 6170 | QJsonObject result; |
| 6171 | result[QStringLiteral("ok")] = false; |
| 6172 | result[QStringLiteral("compileError")] = |
| 6173 | compiled.property(QStringLiteral("message")).toString(); |
| 6174 | result[QStringLiteral("line")] = compiled.property(QStringLiteral("lineNumber")).toInt(); |
| 6175 | return CommandResponse::makeSuccess(id, result); |
| 6176 | } |
| 6177 | |
| 6178 | const auto paintFn = engine.globalObject().property(QStringLiteral("paint")); |
| 6179 | if (!paintFn.isCallable()) { |
| 6180 | QJsonObject result; |
| 6181 | result[QStringLiteral("ok")] = false; |
| 6182 | result[QStringLiteral("compileError")] = |
| 6183 | QStringLiteral("Script compiled but did not define paint(ctx, w, h). Painter scripts " |
| 6184 | "MUST define `function paint(ctx, w, h)`. The function is named " |
| 6185 | "`paint`, not `draw` or `render`."); |
| 6186 | return CommandResponse::makeSuccess(id, result); |
| 6187 | } |
| 6188 | |
| 6189 | const auto onFrameFn = engine.globalObject().property(QStringLiteral("onFrame")); |
| 6190 | QJsonObject result; |
| 6191 | result[QStringLiteral("ok")] = true; |
| 6192 | result[QStringLiteral("hasPaint")] = true; |
| 6193 | result[QStringLiteral("hasOnFrame")] = onFrameFn.isCallable(); |
| 6194 | result[QStringLiteral("hint")] = |
| 6195 | QStringLiteral("Compile + paint() lookup succeeded. Note: dry-run does NOT actually " |
| 6196 | "render -- runtime errors inside paint() (out-of-bounds reads, missing " |
| 6197 | "moveTo before arc, etc.) only surface when the live widget mounts."); |
| 6198 | return CommandResponse::makeSuccess(id, result); |