Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions apps/vscode/src/extension/editor/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,9 @@
* see this files license find the nearest LICENSE file up the source tree.
*/
import { type Mutation } from "@triplex/server";
import { toJSONString } from "@triplex/lib";
import * as vscode from "vscode";

function toJSONString(value: unknown): string {
const str = JSON.stringify(value, (_k, v) =>
v === undefined ? "__UNDEFINED__" : v,
);

return str.replaceAll('"__UNDEFINED__"', "undefined");
}

export class TriplexDocument implements vscode.CustomDocument {
private _onDidChange = new vscode.EventEmitter<{
label: string;
Expand Down
43 changes: 29 additions & 14 deletions packages/@triplex/editor-next/src/features/panels/inputs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
import {
CheckIcon,
CodeIcon,
ExclamationTriangleIcon,
SwitchIcon,
} from "@radix-ui/react-icons";
Expand All @@ -16,7 +17,7 @@ import {
BooleanInput,
ColorInput,
LiteralUnionInput,
NumberInput,
NumberOrExpressionInput,
resolveDefaultValue,
StringInput,
TupleInputNext,
Expand Down Expand Up @@ -126,7 +127,7 @@ export const renderPropInputs: RenderInputs = ({
const persistedValue = "value" in prop.prop ? prop.prop.value : undefined;

return (
<NumberInput
<NumberOrExpressionInput
{...prop.prop.tags}
actionId="scene_controls"
defaultValue={resolveDefaultValue(prop.prop, "number")}
Expand All @@ -138,7 +139,7 @@ export const renderPropInputs: RenderInputs = ({
pointerMode="capture"
required={prop.prop.required}
>
{({ ref, ...props }, { isActive }) => (
{({ ref, ...props }, { isActive, mode, shouldFocus, toggle }) => (
<>
<Label
description={prop.prop.description}
Expand All @@ -147,19 +148,33 @@ export const renderPropInputs: RenderInputs = ({
>
{prop.prop.name}
</Label>
<input
{...props}
aria-label={prop.prop.label}
className={cn([
!isActive && "invalid:border-danger",
"text-input border-input focus:border-selected bg-input placeholder:text-input-placeholder mb-1 h-[26px] w-full cursor-col-resize rounded-sm border px-[9px] [color-scheme:light_dark] [font-variant-numeric:tabular-nums] focus:cursor-text focus:outline-none",
])}
ref={ref}
type="number"
/>
<div className="mb-1 flex gap-1">
<input
{...props}
aria-label={prop.prop.label}
autoFocus={shouldFocus}
className={cn([
!isActive && "invalid:border-danger",
"text-input border-input focus:border-selected bg-input placeholder:text-input-placeholder mb-1 h-[26px] w-full cursor-col-resize rounded-sm border px-[9px] [color-scheme:light_dark] [font-variant-numeric:tabular-nums] focus:cursor-text focus:outline-none",
])}
ref={ref}
type={mode === "number" ? "number" : "text"}
/>
<IconButton
actionId="contextpanel_input_number_expression_toggle"
icon={CodeIcon}
label={
mode === "number"
? "Switch to expression"
: "Switch to number"
}
onClick={toggle}
spacing="spacious"
/>
</div>
</>
)}
</NumberInput>
</NumberOrExpressionInput>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2022—present Michael Dougall. All rights reserved.
*
* This repository utilizes multiple licenses across different directories. To
* see this files license find the nearest LICENSE file up the source tree.
*/
import { memo } from "react";

const Box = () => {
const pi = Math.PI;
return (
<mesh
position={[Math.sqrt(2), Math.sqrt(2), Math.sqrt(2)]}
rotateX={Math.PI / 2}
rotateY={pi / 2}
>
<boxGeometry args={[1, 1, 1]} />
<meshStandardMaterial color="pink" />
</mesh>
);
};

export default memo(Box);
33 changes: 33 additions & 0 deletions packages/@triplex/server/src/ast/__tests__/type-infer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1201,4 +1201,37 @@ describe("type infer", () => {
]
`);
});
it("should evaluate expressions", () => {
const project = _createProject({
tsConfigFilePath: join(__dirname, "__mocks__/tsconfig.json"),
});
const sourceFile = project.addSourceFileAtPath(
join(__dirname, "__mocks__/prop-expression.tsx"),
);
const sceneObject = getJsxElementAt(sourceFile, 12, 5);
if (!sceneObject) {
throw new Error("not found");
}

const { props } = getJsxElementPropTypes(sceneObject);
const rotateXProp = props.find((prop) => prop.name === "rotateX");
expect(rotateXProp).toEqual(
expect.objectContaining({
value: "Math.PI / 2",
valueKind: "number",
}),
);
const rotateYProp = props.find((prop) => prop.name === "rotateY");
expect(rotateYProp).toEqual(
expect.objectContaining({
valueKind: "unhandled",
}),
);
const positionProp = props.find((prop) => prop.name === "position");
expect(positionProp).toEqual(
expect.objectContaining({
value: ["Math.sqrt(2)", "Math.sqrt(2)", "Math.sqrt(2)"],
}),
);
});
});
5 changes: 5 additions & 0 deletions packages/@triplex/server/src/ast/type-infer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* This repository utilizes multiple licenses across different directories. To
* see this files license find the nearest LICENSE file up the source tree.
*/
import { evaluateNumericalExpression } from "@triplex/lib/math";
import {
type AttributeValue,
type DeclaredProp,
Expand Down Expand Up @@ -464,6 +465,10 @@ export function resolveExpressionValue(
return { kind: "number", value: Number(expression.getLiteralText()) };
}

if (expression && evaluateNumericalExpression(expression.getText())) {
return { kind: "number", value: expression.getText() };
}

if (Node.isPrefixUnaryExpression(expression)) {
const operand = expression.getOperand();
if (Node.isNumericLiteral(operand)) {
Expand Down
5 changes: 5 additions & 0 deletions packages/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
"module": "./src/log.ts",
"default": "./src/log.ts"
},
"./math": {
"types": "./src/math.ts",
"module": "./src/math.ts",
"default": "./src/math.ts"
},
"./node": {
"types": "./src/node.ts",
"module": "./src/node.ts",
Expand Down
6 changes: 5 additions & 1 deletion packages/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@
* see this files license find the nearest LICENSE file up the source tree.
*/
export { cn } from "./tw-merge";
export { toJSONString } from "./string";
export {
toJSONString,
type RawCodeExpression,
isRawCodeExpression,
} from "./string";
export { useEvent } from "./use-event";
export { type Accelerator, onKeyDown, blockInputPropagation } from "./keyboard";
export {
Expand Down
23 changes: 23 additions & 0 deletions packages/lib/src/math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2022—present Michael Dougall. All rights reserved.
*
* This repository utilizes multiple licenses across different directories. To
* see this files license find the nearest LICENSE file up the source tree.
*/
export function evaluateNumericalExpression(expression: string): number | null {
try {
const func = new Function(`return ${expression}`);
const result = func();

if (
typeof result === "number" &&
!Number.isNaN(result) &&
Number.isFinite(result)
) {
return result;
}
return null;
} catch {
return null;
}
}
32 changes: 28 additions & 4 deletions packages/lib/src/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,36 @@
*/
import { type CSSProperties } from "react";

export interface RawCodeExpression {
__expr: string;
}

export function isRawCodeExpression(
value: unknown,
): value is RawCodeExpression {
return Boolean(value && typeof value === "object" && "__expr" in value);
}

export function toJSONString(value: unknown): string {
const str = JSON.stringify(value, (_k, v) =>
v === undefined ? "__UNDEFINED__" : v,
);
// Handle RawCodeExpression at the root level first
if (isRawCodeExpression(value)) {
return value.__expr;
}

const str = JSON.stringify(value, (_k, v) => {
if (v === undefined) {
return "__UNDEFINED__";
}
// Handle raw code expressions in nested values
if (isRawCodeExpression(v)) {
return `__EXPR__${v.__expr}__EXPR__`;
}
return v;
});

return str.replaceAll('"__UNDEFINED__"', "undefined");
return str
.replaceAll('"__UNDEFINED__"', "undefined")
.replaceAll(/"__EXPR__(.*?)__EXPR__"/g, "$1");
}

export function kebabCase(str: string): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,24 @@
*/

import { compose, on, type RendererElementProps } from "@triplex/bridge/client";
import { isRawCodeExpression } from "@triplex/lib";
import { fg } from "@triplex/lib/fg";
import { evaluateNumericalExpression } from "@triplex/lib/math";
import { useCallback, useEffect, useRef, useState } from "react";

function useForceRender() {
const [, setState] = useState(0);
return useCallback(() => setState((prev) => prev + 1), []);
}

function unwrapPropValue(value: unknown): unknown {
if (isRawCodeExpression(value)) {
const evaluated = evaluateNumericalExpression(value.__expr);
return evaluated !== null ? evaluated : value;
}
return value;
}

export function useTemporaryProps(
meta: RendererElementProps["__meta"],
props: Record<string, unknown>,
Expand All @@ -40,16 +50,18 @@ export function useTemporaryProps(
}
}),
on("request-set-element-prop", (data) => {
const propValue = unwrapPropValue(data.propValue);

if (data.astPath === meta.astPath && fg("selection_ast_path")) {
intermediateProps.current[data.propName] = data.propValue;
intermediateProps.current[data.propName] = propValue;
forceRender();
} else if (
"column" in data &&
data.column === meta.column &&
data.line === meta.line &&
data.path === meta.path
) {
intermediateProps.current[data.propName] = data.propValue;
intermediateProps.current[data.propName] = propValue;
forceRender();
}
}),
Expand Down
1 change: 1 addition & 0 deletions packages/ux/src/inputs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export { BooleanInput } from "./boolean-input";
export { ColorInput } from "./color-input";
export { LiteralUnionInput } from "./literal-union-input";
export { NumberInput } from "./number-input";
export { NumberOrExpressionInput } from "./number-or-expression-input";
export { PropInput } from "./prop-input";
export { StringInput } from "./string-input";
export { TupleInput } from "./tuple-input";
Expand Down
Loading