Skip to content

Commit

Permalink
feat: add GeometryColorSelector
Browse files Browse the repository at this point in the history
This is a capability that potentially solves "Adjustable contrast of the shadow against the backdrop" in the "Suggestions for improvements" section of the README.

It hopefully also makes the extension more useful for users like https://forums.autodesk.com/t5/forma-developer-forum/background-graphic-shadow-or-quot-shadow-study-extension-quot/m-p/12648176#M322 who struggles to see the contrast in their shadow studies.
  • Loading branch information
hakon-matland-adsk committed Mar 20, 2024
1 parent f80bda2 commit b693b38
Show file tree
Hide file tree
Showing 8 changed files with 611 additions and 210 deletions.
6 changes: 5 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
rel="stylesheet"
href="https://app.autodeskforma.eu/design-system/v2/forma/styles/base.css"
/>
<link rel="stylesheet" href="./src/styles.css"/>
<link rel="stylesheet" href="./src/styles.css" />
<script
type="module"
src="https://app.autodeskforma.eu/design-system/v2/weave/components/button/weave-button.js"
Expand All @@ -14,6 +14,10 @@
type="module"
src="https://app.autodeskforma.eu/design-system/v2/weave/components/dropdown/weave-select.js"
></script>
<script
type="module"
src="https://app.autodeskforma.eu/design-system/v2/weave/components/checkbox/weave-checkbox.js"
></script>
<title>Shadow study</title>
</head>
<body>
Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@
},
"dependencies": {
"file-saver": "^2.0.5",
"forma-embedded-view-sdk": "0.44.0",
"forma-embedded-view-sdk": "0.58.0",
"jszip": "3.10.1",
"lodash": "^4.17.21",
"luxon": "^3.4.3",
"preact": "^10.17.1"
"preact": "^10.19.6"
},
"devDependencies": {
"@preact/preset-vite": "^2.5.0",
"@preact/preset-vite": "^2.8.2",
"@types/file-saver": "^2.0.2",
"@types/lodash": "4.14.198",
"@types/luxon": "^3.3.2",
"prettier": "3.0.3",
"typescript": "^5.0.2",
"vite": "^4.5.2"
"typescript": "^5.4.2",
"vite": "^5.1.6"
}
}
2 changes: 2 additions & 0 deletions src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import IntervalSelector from "./components/IntervalSelector";
import ResolutionSelector from "./components/ResolutionSelector";
import TimeSelector from "./components/TimeSelector";
import PreviewButton from "./components/PreviewButton";
import GeometryColorSelector from "./components/GeometryColorSelector";

export default function App() {
const [month, setMonth] = useState(6);
Expand Down Expand Up @@ -32,6 +33,7 @@ export default function App() {
/>
<IntervalSelector interval={interval} setInterval={setInterval} />
<ResolutionSelector resolution={resolution} setResolution={setResolution} />
<GeometryColorSelector />
<PreviewButton
month={month}
day={day}
Expand Down
176 changes: 176 additions & 0 deletions src/components/GeometryColorSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { Forma } from "forma-embedded-view-sdk/auto";
import { useEffect, useState } from "preact/hooks";
import { FormaElement, Urn } from "forma-embedded-view-sdk/elements/types";

/**
*
* @param urn The root urn of the element hierarchy to search
* @param elements Record that must contain all elements part of the hierarchy
* @param filterPredicate Predicate to filter the elements you want to get the paths for
* @param path Pased recursively to build the path of the elements. Should be "root" when calling the function on a proposal URN.
* @returns a list of paths to the elements that match the filterPredicate
*/
function getPathOfElements(
urn: Urn,
elements: Record<Urn, FormaElement>,
filterPredicate: (element: FormaElement) => boolean,
path: string = "root",
): string[] {
const element = elements[urn];
const paths = [];
if (filterPredicate(element)) {
paths.push(path);
}

if (element.children) {
for (const child of element?.children) {
paths.push(
...getPathOfElements(child.urn, elements, filterPredicate, `${path}/${child.key}`),
);
}
}

return paths;
}

/**
* Color the ground texture with a given color
*/
async function colorGround(color: string) {
const bbox = await Forma.terrain.getBbox();
const canvas = document.createElement("canvas");
const width = bbox.max.x - bbox.min.x;
const height = bbox.max.y - bbox.min.y;
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
if (!context) {
return;
}
context.fillStyle = color;
context.fillRect(0, 0, width, height);
return await Forma.terrain.groundTexture.add({
name: "shadow-study",
canvas: canvas,
position: { x: 0, y: 0, z: 0 },
scale: { x: 1, y: 1 },
});
}

/**
* Color elements with a given color
*/
function colorPaths(elementPaths: string[], color: string) {
const pathsToColor = new Map<string, string>();
for (const path of elementPaths) {
pathsToColor.set(path, color);
}

Forma.render.elementColors.set({ pathsToColor });
}

/**
* Will debounce the function call to avoid calling it too often.
* Useful for avoiding color input events to be called too often.
*/
export const debounce = <F extends (...args: any[]) => ReturnType<F>>(func: F, waitFor: number) => {
let timeout: number | undefined;

return (...args: Parameters<F>): Promise<ReturnType<F>> =>
new Promise((resolve) => {
if (timeout) {
clearTimeout(timeout);
}

timeout = setTimeout(() => resolve(func(...args)), waitFor);
});
};

export default function GeometryColorSelector() {
const [shouldPaintGeometry, setShouldPaintGeometry] = useState(false);
const [shouldPaintTerrain, setShouldPaintTerrain] = useState(false);

const [elementPaths, setElementPaths] = useState<string[]>([]);
const [rootUrn, setRootUrn] = useState<Urn | undefined>();
const [geometryColor, setGeometryColor] = useState("#ffffff");
const [terrainColor, setTerrainColor] = useState("#ffffff");

useEffect(() => {
Forma.proposal.getRootUrn().then((rootUrn) => {
setRootUrn(rootUrn as Urn);
});
Forma.proposal.subscribe(async ({ rootUrn }) => {
await Forma.proposal.awaitProposalPersisted();
setRootUrn(rootUrn as Urn);
});
}, []);

useEffect(() => {
if (rootUrn != null) {
Forma.elements.get({ urn: rootUrn as Urn, recursive: true }).then(({ elements }) => {
const paths = getPathOfElements(rootUrn as Urn, elements, () => true);
setElementPaths(paths);
});
}
}, [rootUrn]);

useEffect(() => {
if (shouldPaintTerrain && shouldPaintGeometry) {
colorGround(terrainColor).then(() => {
colorPaths(elementPaths, geometryColor);
});
} else if (shouldPaintTerrain) {
colorGround(terrainColor);
Forma.render.elementColors.clearAll();
} else if (shouldPaintGeometry) {
colorPaths(elementPaths, geometryColor);
Forma.terrain.groundTexture.remove({ name: "shadow-study" });
} else {
Forma.render.elementColors.clearAll();
Forma.terrain.groundTexture.remove({ name: "shadow-study" });
}
}, [shouldPaintTerrain, shouldPaintGeometry, geometryColor, elementPaths, terrainColor]);

return (
<>
<div class="row">
<div class="row-title" style={{ width: "50%" }}>
Color geometry:
</div>
<div class="row-item">
<weave-checkbox
checked={shouldPaintGeometry}
onChange={(e) => setShouldPaintGeometry(e.detail.checked)}
></weave-checkbox>
<input
type="color"
class={"color-picker"}
value={geometryColor}
onInput={debounce((e: Event) => {
if (e.target instanceof HTMLInputElement) setGeometryColor(e.target?.value);
}, 50)}
/>
</div>
</div>
<div class="row">
<div class="row-title" style={{ width: "50%" }}>
Color terrain:
</div>
<div class="row-item">
<weave-checkbox
checked={shouldPaintTerrain}
onChange={(e) => setShouldPaintTerrain(e.detail.checked)}
></weave-checkbox>
<input
type="color"
class={"color-picker"}
value={geometryColor}
onInput={debounce((e: Event) => {
if (e.target instanceof HTMLInputElement) setTerrainColor(e.target?.value);
}, 50)}
/>
</div>
</div>
</>
);
}
51 changes: 30 additions & 21 deletions src/lib/weave.d.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
export declare module "preact/src/jsx" {
namespace JSXInternal {
interface IntrinsicElements {
"weave-button": JSX.HTMLAttributes<HTMLElement> & {
type?: "button" | "submit" | "reset";
variant?: "outlined" | "flat" | "solid";
density?: "high" | "medium";
iconposition?: "left" | "right";
};
"weave-select": JSX.HTMLAttributes<HTMLElement> & {
placeholder?: any;
value: any;
children: JSX.Element[];
onChange: (e: CustomEvent<{ value: string; text: string }>) => void;
};
"weave-select-option": JSX.HTMLAttributes<HTMLElement> & {
disabled?: true;
value: any;
children?: JSX.Element | string;
};
}
namespace JSX {
interface IntrinsicElements {
"weave-button": JSX.HTMLAttributes<HTMLElement> & {
type?: "button" | "submit" | "reset";
variant?: "outlined" | "flat" | "solid";
density?: "high" | "medium";
iconposition?: "left" | "right";
};
"weave-select": JSX.HTMLAttributes<HTMLElement> & {
placeholder?: any;
value: any;
children: JSX.Element[];
onChange: (e: CustomEvent<{ value: string; text: string }>) => void;
};
"weave-select-option": JSX.HTMLAttributes<HTMLElement> & {
disabled?: true;
value: any;
children?: JSX.Element | string;
};
"weave-checkbox": {
onChange?: (e: CustomEvent) => void;
children?: JSX.Element | string;
style?: string;
checked: boolean;
showlabel?: boolean;
label?: string;
value?: string;
key?: string;
disabled?: boolean;
};
}
}
10 changes: 10 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,13 @@ weave-select {

color: #3c3c3cb2;
}

.color-picker {
margin-left: 10px;
width: 40%;
cursor: pointer;
border: none;
block-size: 20px;
padding: 0;
background-color: transparent;
}
7 changes: 3 additions & 4 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,19 @@
"skipLibCheck": true,

/* Bundler mode */
"moduleResolution": "node",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"jsxImportSource": "preact",

/* Linting */
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
}
Loading

0 comments on commit b693b38

Please sign in to comment.