Hello! My name is Joseph, and I can’t seem to get past this error, after so many failed attempts! This plugin is so close to being complete and even after this error gets finished the next issue that could arise is getting the actual symbols along a user-defined path to actually show up on the blank photoshop document. I would love any kind of insights on any of this please! See below for full details on the issue, goal of plugin, and everything I’ve tried! Thanks so much, I’ve spent several days trying to figure this out, and y’all are my last resort! ![]()
Plugin Goal
The goal of this UXP plugin is to draw weather front symbols (triangles for cold fronts, semicircles for warm fronts, etc.) along a user-defined path in Photoshop and be able to customize a few aspects of the weather front.
The Problem
The plugin consistently fails when trying to create a new path for the symbols using the UXP DOM API. The call to document.pathItems.add() results in a Error: NAPI API failure: String expected. This error occurs even with a simplified, hardcoded path, suggesting an issue within the UXP environment rather than the script’s logic, but not totally 100% sure on this.
Environment
- Manifest Version: 4
- Host App: Photoshop
- Minimum Host Version: 27.0
// manifest.json
{
"id": "com.weatherfronts.plugin.fresh",
"name": "Weather Fronts Fresh",
"version": "1.0.0",
"main": "index.html",
"manifestVersion": 4,
"host": [
{
"app": "PS",
"minVersion": "27.0"
}
],
"entrypoints": [
{
"type": "panel",
"id": "mainPanel",
"label": {
"default": "Weather Fronts"
}
}
]
}
Debugging Steps Taken
We have exhaustively debugged this issue and ruled out many potential causes.
-
Initial
batchPlayApproach:- The first implementation used
action.batchPlayto create paths. - This failed with an
Error: NAPI API failure: Number expected. - We tried various data structures for path points (with and without
_unitwrappers, with and withoutforward/backwardhandles) without success.
- The first implementation used
-
Switch to UXP DOM API:
- To avoid
batchPlaycomplexities, we switched to the modern UXP DOM API, usingdocument.pathItems.add(). - This led to a new series of errors.
- To avoid
-
PathPointInfoConstructor Errors:- Initially, we encountered
TypeError: ... is not a constructorwhen trying to instantiatePathPointInfo. - We discovered through logging and inspecting official Adobe samples that the correct import is
const { PathPointInfo, SubPathInfo } = require('photoshop').app;.
- Initially, we encountered
-
PathKindEnum Issue:- After fixing the import, we used
PathKind.CORNERPOINTfor thepathPoint.kindproperty, as shown in Adobe’s samples. - This failed with
TypeError: Cannot read properties of undefined (reading 'CORNERPOINT'). - Through logging, we confirmed that
require('photoshop').app.PathKindwasundefinedin the user’s environment.
- After fixing the import, we used
-
constants.PointKindEnum Issue:- We then tried using the
require('photoshop').constantsmodule. - Logging confirmed that
constants.PointKind.CORNERPOINTresolves to the string"cornerPoint". - When using this enum, the error reverted to
Error: NAPI API failure: String expected.
- We then tried using the
-
String Literal Attempts:
- Based on the logs and the error message, we tried passing string literals directly to the
pathPoint.kindproperty. - Both
"cornerPoint"and"CORNER_POINT"resulted in the sameError: NAPI API failure: String expected.
- Based on the logs and the error message, we tried passing string literals directly to the
-
Simplified Hardcoded Test:
- To eliminate any possibility of calculation errors, we replaced the dynamic symbol drawing function with one that creates a simple, hardcoded triangle:
[[100, 100], [200, 100], [150, 200]]. - This simplified test case still failed with the exact same
Error: NAPI API failure: String expectederror. This is the strongest evidence that the issue is not with the script’s logic, but with the API call itself in the user’s environment (potentially).
- To eliminate any possibility of calculation errors, we replaced the dynamic symbol drawing function with one that creates a simple, hardcoded triangle:
Conclusion
The pathItems.add() method appears to be broken or behaving in an undocumented way in this specific UXP environment. The parameters being passed to it are correct according to the official documentation and logs, yet the call fails in the native layer with a misleading “String expected” error.
Final Code (index.js)
This is the final state of the code, which is as correct as possible according to the documentation. It fails on the doc.pathItems.add() line.
const { core, app, action } = require('photoshop');
const { PathPointInfo, SubPathInfo } = require('photoshop').app;
document.addEventListener("DOMContentLoaded", () => {
console.log("Plugin DOM Loaded. Ready.");
// --- Get All UI Elements ---
const statusEl = document.getElementById("status");
const drawButton = document.getElementById("btnDrawFront");
const frontTypeButtons = document.querySelectorAll(".front-type-btn");
// Customization Elements
const sizeSlider = document.getElementById("slider-size");
const spacingSlider = document.getElementById("slider-spacing");
const colorPicker = document.getElementById("color-picker");
// --- State Variables (Our plugin's 'memory') ---
let currentFrontValue = "cold";
let currentFrontName = "Cold Front";
let currentSymbolSize = 10;
let currentSymbolSpacing = 25;
let currentFrontColor = "#FF0000";
// --- Functions ---
const getFrontName = (value) => {
switch(value) {
case "cold": return "Cold Front";
case "warm": return "Warm Front";
case "stationary": return "Stationary Front";
case "occluded": return "Occluded Front";
default: return "Unknown Front";
}
};
const updateSelection = (event) => {
frontTypeButtons.forEach(btn => btn.classList.remove('active'));
const clickedButton = event.currentTarget;
clickedButton.classList.add('active');
currentFrontValue = clickedButton.getAttribute('value');
currentFrontName = getFrontName(currentFrontValue);
if (statusEl) {
statusEl.textContent = currentFrontName;
console.log(`Selection updated to: ${currentFrontName}`);
}
};
const drawTriangle = async (p1, angle, symbolSize, layer) => {
const trianglePoints = [
{ x: 0, y: -symbolSize },
{ x: -symbolSize / 2, y: 0 },
{ x: symbolSize / 2, y: 0 }
];
const transformedTriangle = trianglePoints.map(p => {
const rotatedX = p.x * Math.cos(angle) - p.y * Math.sin(angle);
const rotatedY = p.x * Math.sin(angle) + p.y * Math.cos(angle);
return { x: rotatedX + p1.x, y: rotatedY + p1.y };
});
const doc = app.activeDocument;
const pathPoints = transformedTriangle.map(p => {
const point = [Math.round(p.x), Math.round(p.y)];
const pathPoint = new PathPointInfo();
pathPoint.anchor = point;
pathPoint.leftDirection = point;
pathPoint.rightDirection = point;
pathPoint.kind = "cornerPoint";
return pathPoint;
});
const subPathInfo = new SubPathInfo();
subPathInfo.closed = true;
subPathInfo.entireSubPath = pathPoints;
const tempPath = await doc.pathItems.add("tempTriangle", [subPathInfo]);
doc.activeLayers = [layer];
await tempPath.select();
await doc.selection.fill(app.foregroundColor);
await doc.selection.deselect();
await tempPath.remove();
};
const drawSemicircle = async (p1, angle, symbolSize, flipped, layer) => {
const numPoints = 16;
const semicirclePoints = [];
for (let i = 0; i <= numPoints; i++) {
const pointAngle = (i / numPoints) * Math.PI;
const x = Math.cos(pointAngle) * symbolSize;
const y = Math.sin(pointAngle) * symbolSize * (flipped ? -1 : 1);
semicirclePoints.push({ x, y });
}
const transformedSemicircle = semicirclePoints.map(p => {
const rotatedX = p.x * Math.cos(angle) - p.y * Math.sin(angle);
const rotatedY = p.x * Math.sin(angle) + p.y * Math.cos(angle);
return { x: rotatedX + p1.x, y: rotatedY + p1.y };
});
const doc = app.activeDocument;
const pathPoints = transformedSemicircle.map(p => {
const point = [Math.round(p.x), Math.round(p.y)];
const pathPoint = new PathPointInfo();
pathPoint.anchor = point;
pathPoint.leftDirection = point;
pathPoint.rightDirection = point;
pathPoint.kind = "cornerPoint";
return pathPoint;
});
const subPathInfo = new SubPathInfo();
subPathInfo.closed = true;
subPathInfo.entireSubPath = pathPoints;
const tempPath = await doc.pathItems.add("tempSemicircle", [subPathInfo]);
doc.activeLayers = [layer];
await tempPath.select();
await doc.selection.fill(app.foregroundColor);
await doc.selection.deselect();
await tempPath.remove();
};
const drawFront = async (coordinates, frontType, symbolSize, symbolSpacing, color) => {
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null;
};
const rgbColor = hexToRgb(color);
if (!rgbColor) {
await core.showAlert({ title: "Error", message: "Invalid color format." });
return;
}
try {
await core.executeAsModal(async () => {
const document = app.activeDocument;
const newLayer = await document.createLayer({ name: "Weather Front", opacity: 100, blendMode: "normal" });
if (!newLayer) {
throw new Error("Failed to create the 'Weather Front' layer.");
}
await action.batchPlay([{ _obj: "set", _target: [{ _ref: "foregroundColor" }], to: { _obj: "RGBColor", red: rgbColor.r, green: rgbColor.g, blue: rgbColor.b } }], {});
let totalDistanceTraveled = 0;
let distanceToNextSymbol = symbolSpacing;
let symbolCounter = 0;
for (let i = 0; i < coordinates.length - 1; i++) {
const p1 = coordinates[i];
const p2 = coordinates[i + 1];
const segmentDx = p2.x - p1.x;
const segmentDy = p2.y - p1.y;
const segmentLength = Math.sqrt(segmentDx * segmentDx + segmentDy * segmentDy);
if (segmentLength === 0) continue;
const angle = Math.atan2(segmentDy, segmentDx);
while (distanceToNextSymbol <= totalDistanceTraveled + segmentLength) {
const distanceFromP1 = distanceToNextSymbol - totalDistanceTraveled;
const pointOnSegment = { x: p1.x + distanceFromP1 * Math.cos(angle), y: p1.y + distanceFromP1 * Math.sin(angle) };
if (frontType === 'cold') await drawTriangle(pointOnSegment, angle, symbolSize, newLayer);
else if (frontType === 'warm') await drawSemicircle(pointOnSegment, angle, symbolSize, false, newLayer);
else if (frontType === 'stationary') {
if (symbolCounter % 2 === 0) await drawTriangle(pointOnSegment, angle, symbolSize, newLayer);
else await drawSemicircle(pointOnSegment, angle, symbolSize, true, newLayer);
} else if (frontType === 'occluded') {
await drawTriangle(pointOnSegment, angle, symbolSize, newLayer);
await drawSemicircle(pointOnSegment, angle, symbolSize, false, newLayer);
}
symbolCounter++;
distanceToNextSymbol += symbolSpacing;
}
totalDistanceTraveled += segmentLength;
}
await action.batchPlay([{ _obj: "selectNoPath" }], {});
}, { commandName: "Draw Weather Front" });
await core.showAlert({ title: "Success", message: "Drawing complete." });
} catch (e) {
console.error(e);
await core.showAlert({ title: "Error", message: "An unexpected error occurred. See console for details." });
}
};
const onGetPathAndDrawClick = async () => {
try {
const activePathResult = await action.batchPlay([{ _obj: "get", _target: [{ _ref: "path", _enum: "ordinal", _value: "targetEnum" }] }], {});
if (!activePathResult || !activePathResult[0] || !activePathResult[0].pathContents) {
await core.showAlert({ title: "Error", message: "Could not get active path. Please make sure you have a path selected."});
return;
}
const pathData = activePathResult[0].pathContents;
if (!pathData.pathComponents || !pathData.pathComponents[0].subpathListKey || !pathData.pathComponents[0].subpathListKey[0].points || pathData.pathComponents[0].subpathListKey[0].points.length < 2) {
await core.showAlert({ title: "Error", message: "The selected path is invalid or has too few points."});
return;
}
const points = pathData.pathComponents[0].subpathListKey[0].points;
const coordinates = points.map(p => ({ x: p.anchor.horizontal._value, y: p.anchor.vertical._value }));
await drawFront(coordinates, currentFrontValue, currentSymbolSize, currentSymbolSpacing, currentFrontColor);
} catch (e) {
console.error(e);
await core.showAlert({ title: "Error", message: e.message });
}
};
// --- Attach All Event Listeners ---
frontTypeButtons.forEach(button => button.addEventListener("click", updateSelection));
sizeSlider.addEventListener("input", (event) => { currentSymbolSize = parseFloat(event.target.value); console.log("Size changed:", currentSymbolSize); });
spacingSlider.addEventListener("input", (event) => { currentSymbolSpacing = parseFloat(event.target.value); console.log("Spacing changed:", currentSymbolSpacing); });
colorPicker.addEventListener("input", (event) => { currentFrontColor = event.target.color.toHex(true); console.log("Color changed:", currentFrontColor); });
drawButton.addEventListener("click", onGetPathAndDrawClick);
// --- Set Initial State on Load ---
if (statusEl) {
statusEl.textContent = currentFrontName;
}
});