From ca301546d027dc649527d5044d44607a8eb5ef17 Mon Sep 17 00:00:00 2001 From: ck Date: Thu, 22 Oct 2026 06:28:58 +0800 Subject: [PATCH] Show the colour of a shape in the shape library The library's pictures were grey, so the list did not say what colour a shape would land in. Each entry now wears the colour a new shape of its kind starts with: the catalogue's own, or the colour saved in the shape defaults. The grey render keeps its shading. A colour layer masked by the picture outlines multiplies over it, in the list, in the icon-only size and in both themes. shapeAssetColor() is the single rule, shared with makeShapeFromAsset(), and tested in shapeCatalog.test.ts. --- .../public/assets/theme/graphite-theme.css | 4 +++ apps/web/src/app/globals.css | 33 ++++++++++++++++++- apps/web/src/components/LayerlingEditor.tsx | 14 ++++++-- apps/web/src/lib/shapeCatalog.ts | 11 ++++++- docs/CHANGELOG.md | 2 ++ tests/unit/shapeCatalog.test.ts | 16 ++++++++- 6 files changed, 75 insertions(+), 5 deletions(-) diff --git a/apps/web/public/assets/theme/graphite-theme.css b/apps/web/public/assets/theme/graphite-theme.css index 2a25b0ea3..7072b2f4b 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 232024c87..a91eaea68 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 0ffe2c78d..450d79234 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 e56456b62..002882e3a 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 1c4d8b241..28a4446fd 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 8f9a020be..294d7a9ef 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"); + }); +});