diff --git a/apps/web/public/assets/theme/graphite-theme.css b/apps/web/public/assets/theme/graphite-theme.css
index 2a25b0ea..7072b2f4 100644
--- a/apps/web/public/assets/theme/graphite-theme.css
+++ b/apps/web/public/assets/theme/graphite-theme.css
@@ -1868,6 +1868,10 @@ html[data-theme="dark"]:where([data-palette="graphite"]) .shell-openings-summary
border: 1px solid #dddddd;
}
+:where(html[data-theme="dark"][data-palette="graphite"]) .shape-menu-item .shape-menu-thumb::after {
+ background: var(--shape-default-color);
+}
+
:where(html[data-theme="dark"][data-palette="graphite"]) .shape-menu-dropdown:not(.sketch-shape-menu-dropdown) {
--shape-menu-columns: 4;
--shape-menu-width: calc(var(--shape-menu-columns) * 210px + (var(--shape-menu-columns) - 1) * 4px + 34px);
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index 232024c8..a91eaea6 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -4750,6 +4750,37 @@ html[data-theme="dark"] .shell-openings-summary {
font-size: 0.85em;
}
+/* The colour a library entry lands in: its own, or the colour its kind saves in the shape defaults.
+ The grey icon keeps its shading; a colour layer masked by the icon's outline multiplies over it. */
+.shape-menu-item .shape-menu-thumb {
+ position: relative;
+ display: block;
+ flex: 0 0 auto;
+ width: 48px;
+ height: 48px;
+ max-width: none;
+ isolation: isolate;
+ pointer-events: none;
+}
+
+.shape-menu-item .shape-menu-thumb img {
+ display: block;
+ width: 48px;
+ height: 48px;
+ object-fit: contain;
+}
+
+.shape-menu-item .shape-menu-thumb::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: var(--shape-default-color);
+ -webkit-mask: var(--shape-icon) center / contain no-repeat;
+ mask: var(--shape-icon) center / contain no-repeat;
+ mix-blend-mode: multiply;
+ pointer-events: none;
+}
+
/* Custom shapes above the basic shapes (#109). Both scroll together in the
menu body, so the library list itself no longer scrolls on its own there. */
.shape-menu-body {
@@ -4848,7 +4879,7 @@ html[data-simple-mode="on"]:not([data-toolbar-size]) .shape-menu-dropdown:not(.s
padding: 5px;
}
-.shape-menu-icons-only .shape-menu-item > span:not(.my-shape-no-preview) {
+.shape-menu-icons-only .shape-menu-item > span:not(.my-shape-no-preview):not(.shape-menu-thumb) {
display: none;
}
diff --git a/apps/web/src/components/LayerlingEditor.tsx b/apps/web/src/components/LayerlingEditor.tsx
index 0ffe2c78..450d7923 100644
--- a/apps/web/src/components/LayerlingEditor.tsx
+++ b/apps/web/src/components/LayerlingEditor.tsx
@@ -201,7 +201,7 @@ import {
type LylFileHandle,
} from "@/lib/lylFileAccess";
import { simplifyTrianglePositions } from "@/lib/meshSimplify";
-import { clipboardShape, displayShapeName, iconTagAsset, qrCodeAsset, libraryShapeAssets, makeShapeFromAsset, nameTagAsset, shapeAssetLabel, shapeAssetMenuLabel, toolbarShapeAssets } from "@/lib/shapeCatalog";
+import { clipboardShape, displayShapeName, iconTagAsset, qrCodeAsset, libraryShapeAssets, makeShapeFromAsset, nameTagAsset, shapeAssetColor, shapeAssetLabel, shapeAssetMenuLabel, toolbarShapeAssets } from "@/lib/shapeCatalog";
import { importedShapeFromStl } from "@/lib/stlImport";
import { exportMeshesToStl } from "@/lib/stlExport";
import { exportMeshesTo3mf, THREE_MF_MEDIA_TYPE } from "@/lib/threemfExport";
@@ -12511,6 +12511,7 @@ export function LayerlingEditor({
}
addShape(shape);
}}
+ shapeCustomizations={workspaceSettings.shapeCustomizations}
/>
{outlinerOpen ? (
@@ -13440,6 +13441,7 @@ function SecondaryToolbar({
overhangAngle,
onToggleOverhangs,
renderMyShapes,
+ shapeCustomizations,
objects,
onSelectObject,
}: {
@@ -13565,6 +13567,8 @@ function SecondaryToolbar({
onToggleOverhangs: () => void;
/** Custom shapes above the library; gets the way to close the menu. */
renderMyShapes?: (close: () => void) => ReactNode;
+ /** The shape defaults, so each library entry can show the colour it will be added in. */
+ shapeCustomizations: WorkplaneWorkspaceSettings["shapeCustomizations"];
/** The bodies of the design, so the command search can find them by name. */
objects: ReadonlyArray<{ id: string; name: string; kind: string; hidden: boolean }>;
onSelectObject: (id: string) => void;
@@ -14058,6 +14062,7 @@ function SecondaryToolbar({
type="button"
title={shapeAssetMenuLabel(shape).replace(/\s+/g, " ")}
aria-label={shapeAssetMenuLabel(shape).replace(/\s+/g, " ")}
+ style={{ "--shape-default-color": shapeAssetColor(shape, shapeCustomizations[shape.kind]) } as CSSProperties}
draggable
onClick={() => {
if (suppressNextShapeClickRef.current) {
@@ -14113,7 +14118,12 @@ function SecondaryToolbar({
}}
onDragEnd={closeShapesAfterPick}
>
-

+
+
+
word.length > 13) ? "long-word" : undefined}>{shapeAssetMenuLabel(shape)}
))}
diff --git a/apps/web/src/lib/shapeCatalog.ts b/apps/web/src/lib/shapeCatalog.ts
index e56456b6..002882e3 100644
--- a/apps/web/src/lib/shapeCatalog.ts
+++ b/apps/web/src/lib/shapeCatalog.ts
@@ -317,6 +317,15 @@ export const qrCodeAsset: ToolbarShapeAsset = {
*/
export const libraryShapeAssets: ToolbarShapeAsset[] = toolbarShapeAssets.flatMap((asset) => (asset.id === "text" ? [asset, nameTagAsset, iconTagAsset, qrCodeAsset] : asset.id === "doubleCountersink" ? [] : [asset]));
+/**
+ * The colour a new shape of this asset starts with: the asset's own, unless the shape
+ * defaults saved for its kind name another. The library shows the same colour beside each
+ * entry, so the list tells what will land on the workplane.
+ */
+export function shapeAssetColor(asset: ShapeAsset, customization: ShapeCustomization = {}): string {
+ return customization.color ?? asset.color;
+}
+
/**
* Kinds a shape dragged from the library may have. Taken from the library
* itself: a hand-kept list here missed the rounded box and the ruler, and
@@ -865,7 +874,7 @@ export function makeShapeFromAsset(
id: createLocalId(asset.id),
name: asset.name,
kind: asset.kind,
- color: customization.color ?? asset.color,
+ color: shapeAssetColor(asset, customization),
hole: asset.hole,
x: point?.x ?? 0,
z: point?.z ?? 0,
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 1c4d8b24..28a4446f 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -6,6 +6,8 @@ code still carries it - so a lower number further down is older, not newer.
## Unreleased
+- **The shape library shows each shape in the colour it will be added in:** The pictures were grey, so the list said nothing about the colour that landed on the workplane. Every entry now wears the colour a new shape of it starts with - the catalogue's own, or the one saved for its kind under Shape defaults - so a red box, an orange cylinder and a blue sphere are recognisable before the click. The grey render keeps its shading: a colour layer, masked by the picture's outline, multiplies over it, in the list, in the icon-only size and in both themes. `shapeAssetColor` in `shapeCatalog.ts` is the one place that decides the colour, shared with `makeShapeFromAsset`; unit tests in `shapeCatalog.test.ts`; checked in the dev server in light, dark and icon-only menu.
+
## 1.99.0
- **Rounded box radii to 0.01 mm (#705):** `normalizeCornerFillet` and `normalizeTopBottomFillet` rounded to a tenth of a millimetre, so a corner fillet of 0.25 mm came back as 0.3 mm. They round to a hundredth now, and the fields (the shape's properties and the defaults in the workspace settings) step by 0.05. The CAD solid reads the same functions, so it follows. Unit test in `roundedBoxGeometry.test.ts` (0.25 stays 0.25, 7.336 becomes 7.34). Reported by @Algotz on GitHub.
diff --git a/tests/unit/shapeCatalog.test.ts b/tests/unit/shapeCatalog.test.ts
index 8f9a020b..294d7a9e 100644
--- a/tests/unit/shapeCatalog.test.ts
+++ b/tests/unit/shapeCatalog.test.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import type { ShapeAsset } from "@/types/layerling";
-import { clipboardShape, makeShapeFromAsset, parseDroppedShapeAsset, sceneShape, toolbarShapeAssets } from "@/lib/shapeCatalog";
+import { clipboardShape, makeShapeFromAsset, parseDroppedShapeAsset, sceneShape, shapeAssetColor, toolbarShapeAssets } from "@/lib/shapeCatalog";
describe("shape catalog", () => {
it("exposes roundedBox directly after box in the toolbar catalog", () => {
@@ -330,3 +330,17 @@ describe("clipboardShape (#642)", () => {
expect(clipboardShape("box")).toBeNull();
});
});
+
+describe("the colour the library shows", () => {
+ const box = toolbarShapeAssets.find((asset) => asset.id === "box")!;
+
+ it("is the asset's own until the shape defaults name another", () => {
+ expect(shapeAssetColor(box)).toBe(box.color);
+ expect(shapeAssetColor(box, { color: "#123456" })).toBe("#123456");
+ });
+
+ it("is the same colour the shape is added in", () => {
+ expect(makeShapeFromAsset(box).color).toBe(box.color);
+ expect(makeShapeFromAsset(box, undefined, { color: "#123456" }).color).toBe("#123456");
+ });
+});