Skip to content

Add input validation and coercion across API modules#434

Merged
tracygardner merged 6 commits intomainfrom
claude/review-api-validation-VG3Df
Mar 20, 2026
Merged

Add input validation and coercion across API modules#434
tracygardner merged 6 commits intomainfrom
claude/review-api-validation-VG3Df

Conversation

@tracygardner
Copy link
Copy Markdown
Contributor

@tracygardner tracygardner commented Mar 20, 2026

Summary

This PR adds comprehensive input validation and type coercion across multiple API modules to improve robustness and prevent runtime errors from invalid or unexpected parameter values.

Key Changes

api/shapes.js

  • Added validateShapeId() helper to validate shape/mesh IDs (non-empty string, max 100 chars)
  • Added toDim() helper to coerce dimension values to positive finite numbers with fallback
  • Applied validation and coercion to all shape creation methods: createBox, createSphere, createCylinder, createCapsule, createPlane, and create3DText
  • Fixed parameter destructuring to use default empty object = {} for optional parameters
  • Added alpha channel validation (clamped to 0-1 range)
  • Added text validation for create3DText

api/transform.js

  • Added toFinite() helper to coerce values to finite numbers with fallback
  • Enhanced setBlockPositionOnMesh() to validate and coerce x, y, z coordinates while preserving the "__ground__level__" sentinel value
  • Added input validation to moveByVector(), rotate(), rotateTo(), and scale() methods
  • Scale values now validated to be positive finite numbers (default to 1)

api/animate.js

  • Added duration and coordinate validation to animateRotation() and animateGlide()
  • Added safety check for easing function existence before attempting to instantiate
  • Duration defaults to 1 second if invalid; coordinates default to 0

api/sound.js

  • Added volume validation (clamped to 0-1 range) and playbackRate validation (positive finite number)
  • Added soundName validation in playSound()
  • Added BPM validation in setBPM() (defaults to 60)
  • Changed error handling in setBPM() from throwing to warning and graceful return

api/events.js

  • Added callback/handler function type validation to onEvent(), whenActionEvent(), and whenKeyEvent()

api/scene.js

  • Added validation for sourceMeshName and cloneId parameters in cloneMesh()
  • Fixed parameter destructuring to use default empty object

api/control.js

  • Added duration validation in wait() method (non-negative finite number)
  • Added function type validation in waitUntil()

Implementation Details

  • All validation uses Number.isFinite() for robust numeric checks
  • Invalid inputs are logged via console.warn() for debugging
  • Functions return early with sensible defaults (null, 0, 1, etc.) rather than throwing errors
  • Numeric coercion uses Number() constructor followed by range/type validation
  • Maintains backward compatibility while preventing silent failures

https://claude.ai/code/session_01GFLGzae6RMuSSnEo5ax2vH

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened input validation across animations, controls, events, shapes, sound, scene, and transform APIs.
    • Sanitized numeric parameters (durations, positions, sizes, volumes, rates, BPM, delays) with clamping/defaults to prevent NaN/Infinity and overflow.
    • Preserved special ground-level sentinel where applicable and refined easing application so non-linear easing is only used when valid.
    • Replaced throws with graceful early returns and console warnings for invalid/missing inputs.
  • Refactor

    • Added centralized helpers for coercion and normalization of inputs.

- shapes.js: validate IDs (non-empty string, ≤100 chars) for all create
  functions; add sensible defaults and toDim() clamping for dimensions
  that previously had no defaults (cylinder, capsule, plane); clamp alpha
  to [0,1]; validate text/size/depth in create3DText
- animate.js: clamp duration to a positive finite number in rotateAnim
  and glideTo; coerce x/y/z to finite; guard easing string before calling
  flock.BABYLON[easing]() to prevent crash on unknown easing name
- sound.js: clamp volume to [0,1] and playbackRate to positive in
  playSound; validate soundName is a non-empty string; clamp bpm and
  convert setBPM throw to a warn+resolve so it no longer crashes
- events.js: guard handler/callback is a function before registering in
  onEvent, whenActionEvent and whenKeyEvent
- control.js: coerce wait() duration to a non-negative finite ms value;
  guard waitUntil() conditionFunc is a function before polling
- transform.js: add toFinite() helper; coerce x/y/z in positionAt,
  moveByVector, rotate, rotateTo and scale to finite numbers with
  sensible defaults so non-finite inputs never reach Babylon.js
- scene.js: validate sourceMeshName and cloneId in cloneMesh (non-empty
  string, ≤100 chars) before proceeding

https://claude.ai/code/session_01GFLGzae6RMuSSnEo5ax2vH
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 20, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds defensive input validation and numeric sanitization across multiple API modules: coerces numeric inputs, validates string IDs and callback types, applies defaults, and returns early with warnings on invalid inputs.

Changes

Cohort / File(s) Summary
Animation
api/animate.js
Coerces duration and x,y,z to finite numbers (fallbacks applied); preserves "__ground__level__" for y in glideTo; only constructs/applies easing when easing !== "Linear" AND flock.BABYLON[easing] is a function whose prototype inherits from flock.BABYLON.EasingFunction.
Transforms & Movement
api/transform.js
Adds toFinite helper; coerces/normalizes x,y,z in positionAt, moveByVector, rotate, rotateTo; scale sanitizes components to finite >=0 with safe defaults; preserves ground-level sentinel for y.
Control Flow
api/control.js
wait(duration) coerces to finite, non-negative ms (clamped ≤2147483647); waitUntil(conditionFunc) validates conditionFunc is a function, warns and returns resolved Promise on invalid input.
Events
api/events.js
Adds runtime type checks for event handlers (onEvent, whenActionEvent, whenKeyEvent); invalid handler arguments log a warning and return early without registering observers.
Shapes / Asset Creation
api/shapes.js
Adds validateShapeId, toDim, toAlpha; defaults options = {} for factories; validates IDs/text, coerces dimensions/tessellation/alpha with clamping and fallbacks; factories return null on invalid inputs.
Scene (cloning)
api/scene.js
cloneMesh parameter object now defaults to {}; validates sourceMeshName and cloneId (non-empty strings ≤100 chars), logs and returns null for invalid values; validates callback type.
Sound / Audio
api/sound.js
Validates soundName; coerces/clamps volume to [0,1] and playbackRate to finite positive; setBPM sanitizes bpm and resolves (with warning) instead of throwing when mesh not found.

Sequence Diagram(s)

(Skipped — changes are defensive validation and normalization across functions; no new multi-component control flow introduced.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I nibble at inputs, tidy and bright,
numbers rounded, IDs checked right.
Warnings I whisper, defaults I bring,
hops of safety — a tiny spring. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main focus of the changeset: adding input validation and type coercion across multiple API modules.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/review-api-validation-VG3Df

Comment @coderabbitai help to get the list of available commands and usage tips.

Babylon.js 8.x speculatively dynamic-imports WGSL (WebGPU) shader files
even when the app only uses the standard WebGL engine (BABYLON.Engine).
Those files don't exist in the installed package, so Vite dev mode emits
ERR_FAILED console errors for every shader include requested.

Add a Vite plugin that intercepts any module id containing "ShadersWGSL"
and returns an empty string export, letting the dynamic imports resolve
cleanly. Babylon.js then falls back to its WebGL (GLSL) shaders as normal
with no functional change and no more ERR_FAILED noise in the console.

https://claude.ai/code/session_01GFLGzae6RMuSSnEo5ax2vH
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (1)
api/shapes.js (1)

251-261: JS destructuring defaults don't protect against explicit null — use the nullish coalescing pattern instead.

JS parameter defaults only apply to undefined, not null. While the codebase doesn't currently pass null to these factory methods, using options ?? {} instead of = {} is more defensive and clearer about intent. This pattern ensures the code is robust against future callers or dynamic scenarios where null might be supplied.

Also applies to: Lines 335-345, 417-428, 515-517, 611-611, 693-703
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/shapes.js` around lines 251 - 261, The parameter destructuring in
createBox currently uses an = {} default which won't guard against callers
passing explicit null; change the signature so you accept an options param and
destructure from options ?? {} (e.g., keep createBox(boxId, options) then const
{ width = 1, height = 1, depth = 1, color = "#9932CC", position = new
flock.BABYLON.Vector3(0,0,0), alpha = 1, callback = null } = options ?? {}), and
apply the same nullish-coalescing destructuring fix to the other factory
functions with the same pattern shown in the diff (the later parameter blocks at
the lines referenced).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/animate.js`:
- Around line 238-241: The code currently coerces x/y/z to numbers
unconditionally, which destroys the sentinel string "__ground__level__" used by
glideTo; update the coercion so that x, y, and z are left unchanged if they are
the sentinel string (e.g., value === "__ground__level__"), otherwise coerce to
Number and apply the existing numeric fallback (duration keep existing numeric
check). Locate where duration, x, y, z are normalized (variables named duration,
x, y, z inside glideTo/animate) and change those expressions to first check for
the sentinel string, preserving it, then do Number(...) coercion/fallback; make
the same change for the repeated block at lines 247-250.
- Around line 187-190: The current check uses typeof flock.BABYLON[easing] ===
"function" which can match non-easing constructors and cause setEasingMode on
invalid instances; change the guard to verify the constructor actually extends
Babylon's EasingFunction (e.g., ensure flock.BABYLON[easing] &&
flock.BABYLON[easing].prototype instanceof flock.BABYLON.EasingFunction) or
replace it with a whitelist of known easing constructor names before
instantiating and calling setEasingMode; apply the same fix to the other
occurrence around the 303-308 block so rotateAnimation.setEasingFunction is only
called with valid EasingFunction-derived instances.

In `@api/control.js`:
- Around line 13-15: The computed delay (ms) from duration can exceed Node's
setTimeout ceiling (2,147,483,647 ms) causing a TimeoutOverflowWarning and
unexpected 1ms behavior; update the ms calculation in the wait/delay logic to
clamp to the maximum safe timer value by taking Math.min(the computed
non-negative milliseconds, 2147483647) (keep the existing non-negative check),
and reuse that clamped value wherever ms is used (reference the ms variable and
the wait()/delay code paths that consume it).

In `@api/scene.js`:
- Around line 539-547: cloneMesh currently only validates sourceMeshName and
cloneId but allows a truthy non-function callback that later causes a runtime
error when requestAnimationFrame(() => callback()) runs; update cloneMesh to
check callback and if callback is provided but typeof callback !== 'function'
then log a warning and set callback = null (so only a real function will be
invoked), and apply the same validation to the other occurrence in this file
where requestAnimationFrame(() => callback()) is used (the second callback usage
noted in the review).

In `@api/shapes.js`:
- Around line 704-710: The create3DText branch validates modelId and text but
doesn't guard font before string operations (e.g. font.endsWith(".json")), so
add an early check in the create3DText path: verify font is present and typeof
font === "string" (similar to the text check) and if not, log a warning like
"create3DText: invalid font" and return null; apply the same guard before any
other usage of font (including the later branch that calls
font.endsWith(".json")) so non-string or missing fonts take the graceful-return
path instead of hitting the async loader.
- Around line 611-615: createPlane currently dereferences position without a
safe default, causing createPlane("id") to crash when position is undefined;
update createPlane to normalize position early (before any indexing) by
accepting undefined, array, or Vector3 and converting them to a Vector3 (or
default [0,0,0]) — e.g., add a small normalization step at the top of
createPlane that sets position = normalizePosition(position) or position =
[0,0,0] when falsy, then use the normalized position for indexing; apply the
same normalization pattern to the other similar function referenced (lines
~642-645) so both locations defensively handle undefined, array, and Vector3
inputs.

In `@api/transform.js`:
- Around line 103-111: In positionAt(), ensure x and z are always coerced
through toFinite (use toFinite(x, mesh.position.x) and toFinite(z,
mesh.position.z)) regardless of y === "__ground__level__", and treat y
specially: if y === "__ground__level__" leave the sentinel, otherwise replace y
with a safe numeric fallback via y = toFinite(y, mesh.position.y) (or convert
Number(y) when finite), so no strings/NaN reach the ground raycast or
mesh.position.set; update the code paths around toFinite,
Number.isFinite(Number(y)), and mesh.position.set(...) accordingly.

---

Nitpick comments:
In `@api/shapes.js`:
- Around line 251-261: The parameter destructuring in createBox currently uses
an = {} default which won't guard against callers passing explicit null; change
the signature so you accept an options param and destructure from options ?? {}
(e.g., keep createBox(boxId, options) then const { width = 1, height = 1, depth
= 1, color = "#9932CC", position = new flock.BABYLON.Vector3(0,0,0), alpha = 1,
callback = null } = options ?? {}), and apply the same nullish-coalescing
destructuring fix to the other factory functions with the same pattern shown in
the diff (the later parameter blocks at the lines referenced).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9154fece-0566-4d01-8b3d-af10eebc19dc

📥 Commits

Reviewing files that changed from the base of the PR and between 522448d and d3bb6c8.

📒 Files selected for processing (7)
  • api/animate.js
  • api/control.js
  • api/events.js
  • api/scene.js
  • api/shapes.js
  • api/sound.js
  • api/transform.js

api/animate.js Outdated
Comment on lines +238 to +241
duration = Number.isFinite(Number(duration)) && Number(duration) > 0 ? Number(duration) : 1;
x = Number.isFinite(Number(x)) ? Number(x) : 0;
y = Number.isFinite(Number(y)) ? Number(y) : 0;
z = Number.isFinite(Number(z)) ? Number(z) : 0;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't coerce away __ground__level__.

glideTo(..., { y: "__ground__level__" }) now becomes y = 0 before the sentinel check runs, so valid ground-following calls glide to absolute zero instead of sampled terrain height.

🛠 Suggested fix
-    duration = Number.isFinite(Number(duration)) && Number(duration) > 0 ? Number(duration) : 1;
-    x = Number.isFinite(Number(x)) ? Number(x) : 0;
-    y = Number.isFinite(Number(y)) ? Number(y) : 0;
-    z = Number.isFinite(Number(z)) ? Number(z) : 0;
+    duration =
+      Number.isFinite(Number(duration)) && Number(duration) > 0
+        ? Number(duration)
+        : 1;
+    x = Number.isFinite(Number(x)) ? Number(x) : 0;
+    z = Number.isFinite(Number(z)) ? Number(z) : 0;
+    if (y !== "__ground__level__") {
+      y = Number.isFinite(Number(y)) ? Number(y) : 0;
+    }

Also applies to: 247-250

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/animate.js` around lines 238 - 241, The code currently coerces x/y/z to
numbers unconditionally, which destroys the sentinel string "__ground__level__"
used by glideTo; update the coercion so that x, y, and z are left unchanged if
they are the sentinel string (e.g., value === "__ground__level__"), otherwise
coerce to Number and apply the existing numeric fallback (duration keep existing
numeric check). Locate where duration, x, y, z are normalized (variables named
duration, x, y, z inside glideTo/animate) and change those expressions to first
check for the sentinel string, preserving it, then do Number(...)
coercion/fallback; make the same change for the repeated block at lines 247-250.

Comment on lines +539 to +547
cloneMesh({ sourceMeshName, cloneId, callback = null } = {}) {
if (!sourceMeshName || typeof sourceMeshName !== "string" || sourceMeshName.length > 100) {
console.warn("cloneMesh: invalid sourceMeshName");
return null;
}
if (!cloneId || typeof cloneId !== "string" || cloneId.length > 100) {
console.warn("cloneMesh: invalid cloneId");
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate callback alongside the ID fields.

A truthy non-function still reaches requestAnimationFrame(() => callback()) later in this method and throws there. Since this entrypoint is now doing input hardening, it should null out or warn on invalid callbacks as well.

🛠 Suggested fix
   cloneMesh({ sourceMeshName, cloneId, callback = null } = {}) {
     if (!sourceMeshName || typeof sourceMeshName !== "string" || sourceMeshName.length > 100) {
       console.warn("cloneMesh: invalid sourceMeshName");
       return null;
     }
     if (!cloneId || typeof cloneId !== "string" || cloneId.length > 100) {
       console.warn("cloneMesh: invalid cloneId");
       return null;
     }
+    if (callback != null && typeof callback !== "function") {
+      console.warn("cloneMesh: callback must be a function");
+      callback = null;
+    }

Also applies to: 595-597

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/scene.js` around lines 539 - 547, cloneMesh currently only validates
sourceMeshName and cloneId but allows a truthy non-function callback that later
causes a runtime error when requestAnimationFrame(() => callback()) runs; update
cloneMesh to check callback and if callback is provided but typeof callback !==
'function' then log a warning and set callback = null (so only a real function
will be invoked), and apply the same validation to the other occurrence in this
file where requestAnimationFrame(() => callback()) is used (the second callback
usage noted in the review).

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
vite.config.mjs (1)

17-23: Use resolveId + virtual ID for reliably stubbing missing modules.

The current implementation relies on load(id) which is called only after successful module resolution. If an import fails to resolve, load() is never invoked and the build errors instead of stubbing the missing module. Reliable stubbing requires using resolveId to return a virtual module ID (conventionally prefixed with \0) and handling that virtual ID in load().

Recommended implementation
 const stubMissingBabylonWgslPlugin = {
   name: "stub-missing-babylon-wgsl",
+  async resolveId(source, importer) {
+    if (!/ShadersWGSL|shadersWGSL/.test(source)) return null;
+    const resolved = await this.resolve(source, importer, { skipSelf: true });
+    if (resolved) return null; // keep real module when present
+    return `\0stub-babylon-wgsl:${source}`;
+  },
   load(id) {
-    if (id.includes("ShadersWGSL") || id.includes("shadersWGSL")) {
+    if (id.startsWith("\0stub-babylon-wgsl:")) {
       return "export default '';";
     }
+    return null;
   },
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vite.config.mjs` around lines 17 - 23, The plugin
stubMissingBabylonWgslPlugin currently only implements load(id) so it won’t
catch imports that fail resolution; change the plugin to implement
resolveId(source, importer) and, when source matches "ShadersWGSL" or
"shadersWGSL", return a virtual ID (e.g. '\0virtual:ShadersWGSL'); then update
load(id) to check for that virtual ID and return "export default '';" so
unresolved imports are stubbed reliably. Ensure the plugin name
stubMissingBabylonWgslPlugin still remains the same and only the resolveId and
load handling are added/adjusted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@vite.config.mjs`:
- Around line 12-24: Prettier reported a formatting mismatch in vite.config.mjs;
run your project's Prettier formatter and commit the resulting changes so the
file matches the repo style. Specifically reformat the
stubMissingBabylonWgslPlugin object (its name and load(id) function) to comply
with the project's Prettier rules, ensure consistent quoting/spacing and a final
newline, then add and commit the updated vite.config.mjs.

---

Nitpick comments:
In `@vite.config.mjs`:
- Around line 17-23: The plugin stubMissingBabylonWgslPlugin currently only
implements load(id) so it won’t catch imports that fail resolution; change the
plugin to implement resolveId(source, importer) and, when source matches
"ShadersWGSL" or "shadersWGSL", return a virtual ID (e.g.
'\0virtual:ShadersWGSL'); then update load(id) to check for that virtual ID and
return "export default '';" so unresolved imports are stubbed reliably. Ensure
the plugin name stubMissingBabylonWgslPlugin still remains the same and only the
resolveId and load handling are added/adjusted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99467491-5a5f-4247-a166-ba013972c77c

📥 Commits

Reviewing files that changed from the base of the PR and between d3bb6c8 and 6777429.

📒 Files selected for processing (1)
  • vite.config.mjs

claude added 2 commits March 20, 2026 19:21
The stub matched all module IDs containing "ShadersWGSL", which
incorrectly intercepted greasedLinePluginMaterialShadersWGSL.js — a
real file that exports GetCustomCode. Replacing it with an empty stub
caused a SyntaxError on the static import, preventing @babylonjs/core
from loading and leaving the app stuck on the loading screen.

The original ERR_FAILED console messages for missing WGSL shader
includes are cosmetic (dynamic imports for WebGPU shaders that fall
back silently to WebGL) and are better left alone.

https://claude.ai/code/session_01GFLGzae6RMuSSnEo5ax2vH
toDim was clamping any non-positive value to the fallback (1), so
create_box with height=0 silently produced a height-1 box. The tallest
buildings project relies on boxes starting flat (height 0) and scaling
up as data loads.

Changing n > 0 to n >= 0 keeps the guard against NaN and negative
values while accepting zero as a valid dimension.

https://claude.ai/code/session_01GFLGzae6RMuSSnEo5ax2vH
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/shapes.js (1)

1-1: ⚠️ Potential issue | 🟡 Minor

Pipeline: Prettier formatting check failed.

The CI reports a formatting issue. Run prettier --write api/shapes.js to fix before merging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/shapes.js` at line 1, Run Prettier to fix formatting in api/shapes.js
(the file containing the import earcut statement); execute prettier --write
api/shapes.js (or run your project's formatting script) and commit the resulting
changes so the import line and the rest of the file conform to the project's
Prettier rules.
♻️ Duplicate comments (2)
api/shapes.js (2)

703-710: ⚠️ Potential issue | 🟠 Major

font still not validated before string operations.

The past review comment about this remains unaddressed. If font is omitted or non-string, line 723 (font.endsWith(".json")) throws a TypeError. While caught by the try-catch, it produces a confusing error message and incorrect fallback behavior.

🛡️ Proposed fix: validate font early
   if (!validateShapeId(modelId, "create3DText")) return null;
   if (!text || typeof text !== "string") {
     console.warn("create3DText: invalid text");
     return null;
   }
+  if (!font || typeof font !== "string") {
+    console.warn("create3DText: invalid font");
+    return null;
+  }
   size = toDim(size, 50);
   depth = toDim(depth, 1);

Also applies to: 722-723

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/shapes.js` around lines 703 - 710, In create3DText, validate the font
variable before any string operations: ensure font is present and typeof font
=== "string" (or coerce to a safe default) before calling font.endsWith(".json")
so the function doesn't throw a TypeError; update the logic around the font
variable (the block using font.endsWith(".json") and any fallbacks) to first
check the string type and then proceed to JSON-checking or defaulting, and keep
validateShapeId and existing size/depth handling unchanged.

611-614: ⚠️ Potential issue | 🔴 Critical

position still lacks a default, causing crashes when omitted.

The past review comment about this issue remains unaddressed. Calling createPlane("id") or createPlane("id", { color: "red" }) will throw a TypeError at line 643 when accessing position[0] on undefined.

🐛 Proposed fix: add default for position
-  createPlane(planeId, { color, width, height, position, callback = null } = {}) {
+  createPlane(planeId, { color, width, height, position = [0, 0, 0], callback = null } = {}) {

Also applies to: 642-646

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/shapes.js` around lines 611 - 614, The createPlane function's
destructured parameters lack a default for position causing a TypeError when
callers omit it; update the parameter destructuring for createPlane(planeId, {
color, width, height, position = [0,0,0], callback = null } = {}) or, at the top
of the function (after validateShapeId and toDim calls), ensure position is set
to a safe default like [0,0,0] before any access to position[0]; reference
createPlane, validateShapeId, and toDim to locate the code and make the change
so position is never undefined when used.
🧹 Nitpick comments (1)
api/shapes.js (1)

267-267: Consider extracting alpha clamping to a helper for DRY.

The same alpha clamping logic is repeated in createBox, createSphere, createCylinder, and createCapsule. A small helper would reduce duplication and risk of inconsistency.

♻️ Optional helper extraction
+// Coerce alpha to a finite number in [0, 1], defaulting to 1.
+function toAlpha(v) {
+  const n = Number(v);
+  return Number.isFinite(n) ? Math.max(0, Math.min(1, n)) : 1;
+}

Then replace each occurrence:

-alpha = Number.isFinite(Number(alpha)) ? Math.max(0, Math.min(1, Number(alpha))) : 1;
+alpha = toAlpha(alpha);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/shapes.js` at line 267, Extract the repeated alpha normalization/clamping
logic into a single helper (e.g., normalizeAlpha or clampAlpha) and replace the
inlined line in createBox, createSphere, createCylinder, and createCapsule with
a call to that helper; the helper should accept the raw alpha value, coerce to
Number, return 1 for non-finite inputs, and otherwise return Math.max(0,
Math.min(1, Number(alpha))). Ensure all four functions import/require or
reference the new helper to remove duplication and keep behavior identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@api/shapes.js`:
- Line 1: Run Prettier to fix formatting in api/shapes.js (the file containing
the import earcut statement); execute prettier --write api/shapes.js (or run
your project's formatting script) and commit the resulting changes so the import
line and the rest of the file conform to the project's Prettier rules.

---

Duplicate comments:
In `@api/shapes.js`:
- Around line 703-710: In create3DText, validate the font variable before any
string operations: ensure font is present and typeof font === "string" (or
coerce to a safe default) before calling font.endsWith(".json") so the function
doesn't throw a TypeError; update the logic around the font variable (the block
using font.endsWith(".json") and any fallbacks) to first check the string type
and then proceed to JSON-checking or defaulting, and keep validateShapeId and
existing size/depth handling unchanged.
- Around line 611-614: The createPlane function's destructured parameters lack a
default for position causing a TypeError when callers omit it; update the
parameter destructuring for createPlane(planeId, { color, width, height,
position = [0,0,0], callback = null } = {}) or, at the top of the function
(after validateShapeId and toDim calls), ensure position is set to a safe
default like [0,0,0] before any access to position[0]; reference createPlane,
validateShapeId, and toDim to locate the code and make the change so position is
never undefined when used.

---

Nitpick comments:
In `@api/shapes.js`:
- Line 267: Extract the repeated alpha normalization/clamping logic into a
single helper (e.g., normalizeAlpha or clampAlpha) and replace the inlined line
in createBox, createSphere, createCylinder, and createCapsule with a call to
that helper; the helper should accept the raw alpha value, coerce to Number,
return 1 for non-finite inputs, and otherwise return Math.max(0, Math.min(1,
Number(alpha))). Ensure all four functions import/require or reference the new
helper to remove duplication and keep behavior identical.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e30d7a50-c3ed-456f-b2c5-8c794c00e5e6

📥 Commits

Reviewing files that changed from the base of the PR and between 6777429 and 311b274.

📒 Files selected for processing (1)
  • api/shapes.js

Same n > 0 guard as toDim was silently converting scale Y=0 to 1,
preventing the tallest buildings project from starting boxes flat.

https://claude.ai/code/session_01GFLGzae6RMuSSnEo5ax2vH
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
api/transform.js (1)

103-111: ⚠️ Potential issue | 🟠 Major

positionAt() still leaks unsanitized coordinates.

Line 107 still gates x/z coercion on the non-sentinel path, and Line 110 leaves an invalid non-sentinel y untouched. setBlockPositionOnMesh() can still receive strings/NaN for the ground lookup or mesh.position.set(...).

🛠️ Suggested fix
-        x = x ?? mesh.position.x;
-        y = y ?? mesh.position.y;
-        z = z ?? mesh.position.z;
-        // Allow the ground-level sentinel string through; coerce everything else.
-        if (y !== "__ground__level__") {
-          x = toFinite(x, mesh.position.x);
-          z = toFinite(z, mesh.position.z);
-          if (Number.isFinite(Number(y))) y = Number(y);
-        }
+        x = toFinite(x ?? mesh.position.x, mesh.position.x);
+        z = toFinite(z ?? mesh.position.z, mesh.position.z);
+        y = y ?? mesh.position.y;
+        if (y !== "__ground__level__") {
+          y = toFinite(y, mesh.position.y);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/transform.js` around lines 103 - 111, The positionAt() logic still lets
non-sentinel strings/NaN through to setBlockPositionOnMesh() and
mesh.position.set(...); always coerce x,z (and y when not the
"__ground__level__" sentinel) using toFinite with mesh.position.x/y/z defaults
before any sentinel check, and ensure y is converted to a Number and then
toFinite (so non-numeric strings become the mesh.position.y default rather than
remaining as strings/NaN). Update the block around x = x ?? mesh.position.x; y =
y ?? mesh.position.y; z = z ?? mesh.position.z; to call toFinite(x,
mesh.position.x) and toFinite(z, mesh.position.z) unconditionally, and for y: if
y === "__ground__level__" keep the sentinel, else set y = toFinite(Number(y),
mesh.position.y); this prevents setBlockPositionOnMesh() and mesh.position.set
from receiving invalid types.
🧹 Nitpick comments (1)
api/transform.js (1)

625-627: Guard against explicitly null or empty-string scale values before coercion.

Lines 625–627 coerce values to numbers without checking for null or blank strings, so explicitly passing scale(meshName, {x: null}) or scale(meshName, {x: ""}) would flatten the axis instead of preserving the default 1. While the current destructuring defaults handle omitted parameters correctly, defensive checks for explicit null and empty strings would prevent accidental misuse.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/transform.js` around lines 625 - 627, The numeric coercion for x, y, z in
transform.js currently converts null or empty-string to 0 (flattening the axis);
update the guards in the scale logic (the x, y, z variables used by the scale
function) to treat explicit null and "" as “use default 1” before coercion—i.e.
check x === null || x === "" (and same for y and z) and fallback to 1, otherwise
run the existing Number.isFinite(Number(...)) && Number(...) >= 0 check to
coerce/validate the provided value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@api/transform.js`:
- Around line 103-111: The positionAt() logic still lets non-sentinel
strings/NaN through to setBlockPositionOnMesh() and mesh.position.set(...);
always coerce x,z (and y when not the "__ground__level__" sentinel) using
toFinite with mesh.position.x/y/z defaults before any sentinel check, and ensure
y is converted to a Number and then toFinite (so non-numeric strings become the
mesh.position.y default rather than remaining as strings/NaN). Update the block
around x = x ?? mesh.position.x; y = y ?? mesh.position.y; z = z ??
mesh.position.z; to call toFinite(x, mesh.position.x) and toFinite(z,
mesh.position.z) unconditionally, and for y: if y === "__ground__level__" keep
the sentinel, else set y = toFinite(Number(y), mesh.position.y); this prevents
setBlockPositionOnMesh() and mesh.position.set from receiving invalid types.

---

Nitpick comments:
In `@api/transform.js`:
- Around line 625-627: The numeric coercion for x, y, z in transform.js
currently converts null or empty-string to 0 (flattening the axis); update the
guards in the scale logic (the x, y, z variables used by the scale function) to
treat explicit null and "" as “use default 1” before coercion—i.e. check x ===
null || x === "" (and same for y and z) and fallback to 1, otherwise run the
existing Number.isFinite(Number(...)) && Number(...) >= 0 check to
coerce/validate the provided value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1282bb04-d400-4159-ab3a-4277c2ee9564

📥 Commits

Reviewing files that changed from the base of the PR and between 311b274 and a26c4d4.

📒 Files selected for processing (1)
  • api/transform.js

- createPlane: default position to [0,0,0] to prevent crash when omitted
- glideTo: preserve __ground__level__ sentinel before numeric coercion
- positionAt: always coerce x/z; only coerce y when not the sentinel
- create3DText: validate font before calling .endsWith()
- easing check: guard with instanceof EasingFunction to reject non-easing constructors
- wait(): clamp ms to 2147483647 to avoid silent setTimeout overflow
- cloneMesh: null out non-function callbacks with a warning
- shapes.js: extract toAlpha() helper, replace four inline copies

https://claude.ai/code/session_01GFLGzae6RMuSSnEo5ax2vH
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
api/transform.js (1)

621-623: Consider extracting the coerced value to avoid redundant Number() calls.

The validation logic is correct (non-negative, finite, default to 1), but Number(x) is evaluated up to 4 times per variable. This is a minor inefficiency.

♻️ Suggested refactor
-    x = Number.isFinite(Number(x)) && Number(x) >= 0 ? Number(x) : 1;
-    y = Number.isFinite(Number(y)) && Number(y) >= 0 ? Number(y) : 1;
-    z = Number.isFinite(Number(z)) && Number(z) >= 0 ? Number(z) : 1;
+    {
+      const nx = Number(x), ny = Number(y), nz = Number(z);
+      x = Number.isFinite(nx) && nx >= 0 ? nx : 1;
+      y = Number.isFinite(ny) && ny >= 0 ? ny : 1;
+      z = Number.isFinite(nz) && nz >= 0 ? nz : 1;
+    }

Alternatively, reuse the toDim helper pattern with a custom fallback of 1:

function toNonNegative(v, fallback) {
  const n = Number(v);
  return Number.isFinite(n) && n >= 0 ? n : fallback;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/transform.js` around lines 621 - 623, Extract the repeated Number()
coercion by computing a single numeric value per variable and reuse it (or add a
small helper like toNonNegative(v, fallback)) and then assign x, y, z using that
single numeric check; e.g., for each of x, y, z compute const nx = Number(x) (or
call toNonNegative(x, 1)), test Number.isFinite(nx) && nx >= 0, and set x = nx
or fallback 1, and do the same for y and z to remove the redundant Number()
calls in the assignments shown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@api/transform.js`:
- Around line 621-623: Extract the repeated Number() coercion by computing a
single numeric value per variable and reuse it (or add a small helper like
toNonNegative(v, fallback)) and then assign x, y, z using that single numeric
check; e.g., for each of x, y, z compute const nx = Number(x) (or call
toNonNegative(x, 1)), test Number.isFinite(nx) && nx >= 0, and set x = nx or
fallback 1, and do the same for y and z to remove the redundant Number() calls
in the assignments shown.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36467c32-4025-40ef-afcc-7b35f72984ab

📥 Commits

Reviewing files that changed from the base of the PR and between a26c4d4 and bf19d47.

📒 Files selected for processing (5)
  • api/animate.js
  • api/control.js
  • api/scene.js
  • api/shapes.js
  • api/transform.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/scene.js
  • api/control.js

@tracygardner tracygardner merged commit 1bb8de0 into main Mar 20, 2026
7 checks passed
@tracygardner tracygardner deleted the claude/review-api-validation-VG3Df branch March 20, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants