diff --git a/bench/notes/BENCH.md b/bench/notes/BENCH.md index 95e6d7e5..17f8ea4a 100644 --- a/bench/notes/BENCH.md +++ b/bench/notes/BENCH.md @@ -41,7 +41,6 @@ node bench/lossy-optimizer-bench.mjs --json bench/results/lossy-optimizer.json node bench/lossy-optimizer-bench.mjs --models ducky,shark,bicycle node bench/lossy-corpus-bench.mjs --root /tmp/polycss-model-corpus --json /tmp/polycss-temp-corpus.json node bench/lossy-corpus-bench.mjs --from-json bench/results/lossy-corpus.json --opportunities -node .agents/skills/chrome-trace/scripts/trace.mjs motion --mesh garden --runs 3 --dom-samples --report --markdown-out bench/results/garden-trace.md node bench/perf-visual.mjs --mesh chicken --tolerance 0.005 node bench/nonvoxel-rotation-bench.mjs --models teapot,bicycle --variants baseline,order-tile4 --run-order round-robin node .agents/skills/chrome-trace/scripts/trace.mjs drag --mesh teapot --degrees 360 --drag-ms 1500 --label teapot-drag --frame-details --no-print-json diff --git a/bench/notes/PERF_INVESTIGATION.md b/bench/notes/PERF_INVESTIGATION.md index fcc8f691..bc157093 100644 --- a/bench/notes/PERF_INVESTIGATION.md +++ b/bench/notes/PERF_INVESTIGATION.md @@ -72,7 +72,7 @@ node bench/nonvoxel-visual-compare.mjs | Standard perf | `chicken`, `rock1`, `saucer` | Cross-renderer and dynamic-light smoke. | | Non-voxel rotation | `chicken`, `rock1`, `saucer`, `teapot`, `ducky`, `elephant`, `policecar`, `bicycle` | Broad baked camera-rotation triage. | | Voxel GPU-hard | `AncientCrashSite`, `skyscraper`, long-window `army` | Current target class for voxel renderer work. | -| Voxel counterexamples | `obj_house3`, `scene_mechanic2`, `Treasure`, `desert2`, `Garden` | Catch order, wrapper, and backend-specific false positives. | +| Voxel counterexamples | `obj_house3`, `scene_mechanic2`, `Treasure`, `desert2` | Catch order, wrapper, and backend-specific false positives. | ## Current Baselines diff --git a/bench/perf-shared.mjs b/bench/perf-shared.mjs index 010c7de5..0deadf0b 100644 --- a/bench/perf-shared.mjs +++ b/bench/perf-shared.mjs @@ -73,11 +73,6 @@ export const PRESETS = { options: { targetSize: 60, gridShift: 0 }, zoom: 0.4, rotX: 65, rotY: 45, }, - "garden": { - url: "/gallery/vox/Garden.vox", - options: { targetSize: 60, gridShift: 0 }, - zoom: 0.4, rotX: 65, rotY: 45, - }, "mecha-golem": { url: "/gallery/vox/MechaGolem.vox", options: { targetSize: 60, gridShift: 0 }, diff --git a/website/astro.config.mjs b/website/astro.config.mjs index ad2a46db..e539b69a 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -44,6 +44,15 @@ export default defineConfig({ starlight({ title: 'PolyCSS', description: 'A CSS polygon mesh engine. DOM-native 3D rendering.', + favicon: '/favicon.ico', + head: [ + { tag: 'meta', attrs: { property: 'og:image', content: 'https://polycss.com/polycss-github.png' } }, + { tag: 'meta', attrs: { property: 'og:image:width', content: '1280' } }, + { tag: 'meta', attrs: { property: 'og:image:height', content: '640' } }, + { tag: 'meta', attrs: { property: 'og:image:alt', content: 'PolyCSS logo, a rendered polygon duck, and DOM markup.' } }, + { tag: 'meta', attrs: { name: 'twitter:image', content: 'https://polycss.com/polycss-github.png' } }, + { tag: 'meta', attrs: { name: 'twitter:image:alt', content: 'PolyCSS logo, a rendered polygon duck, and DOM markup.' } }, + ], disable404Route: true, components: { Header: './src/components/DocsHeader.astro', diff --git a/website/public/gallery/glb/nasa/mars-global-surveyor.glb b/website/public/gallery/glb/nasa/mars-global-surveyor.glb deleted file mode 100644 index ef9fe022..00000000 Binary files a/website/public/gallery/glb/nasa/mars-global-surveyor.glb and /dev/null differ diff --git a/website/public/gallery/glb/poly-pizza/character-animated.glb b/website/public/gallery/glb/poly-pizza/character-animated.glb deleted file mode 100644 index 7f178f7d..00000000 Binary files a/website/public/gallery/glb/poly-pizza/character-animated.glb and /dev/null differ diff --git a/website/public/gallery/glb/poly-pizza/rabbit-blond.glb b/website/public/gallery/glb/poly-pizza/rabbit-blond.glb deleted file mode 100644 index 4effe3fe..00000000 Binary files a/website/public/gallery/glb/poly-pizza/rabbit-blond.glb and /dev/null differ diff --git a/website/public/gallery/glb/urban/Adventurer.glb b/website/public/gallery/glb/urban/Adventurer.glb deleted file mode 100644 index 883c5193..00000000 Binary files a/website/public/gallery/glb/urban/Adventurer.glb and /dev/null differ diff --git a/website/public/gallery/vox-manifest.json b/website/public/gallery/vox-manifest.json index 0aed0681..461677ba 100644 --- a/website/public/gallery/vox-manifest.json +++ b/website/public/gallery/vox-manifest.json @@ -56,7 +56,6 @@ "vox/chr_zombie4.vox", "vox/christmas_tree.vox", "vox/couch.vox", - "vox/desert.vox", "vox/desert2.vox", "vox/dog.vox", "vox/door.vox", @@ -68,7 +67,6 @@ "vox/floooh/kc85-computer.vox", "vox/floooh/kc85-keyboard.vox", "vox/floooh/lcr-c.vox", - "vox/Garden.vox", "vox/gifts.vox", "vox/horse.vox", "vox/house.vox", diff --git a/website/public/gallery/vox/Garden.vox b/website/public/gallery/vox/Garden.vox deleted file mode 100644 index e7088829..00000000 Binary files a/website/public/gallery/vox/Garden.vox and /dev/null differ diff --git a/website/public/gallery/vox/desert.vox b/website/public/gallery/vox/desert.vox deleted file mode 100644 index 47dd7e96..00000000 Binary files a/website/public/gallery/vox/desert.vox and /dev/null differ diff --git a/website/public/polycss-github.png b/website/public/polycss-github.png new file mode 100644 index 00000000..516e1a98 Binary files /dev/null and b/website/public/polycss-github.png differ diff --git a/website/public/robots.txt b/website/public/robots.txt new file mode 100644 index 00000000..87debdde --- /dev/null +++ b/website/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://polycss.com/sitemap-index.xml diff --git a/website/src/components/BuilderWorkbench/BuilderWorkbench.tsx b/website/src/components/BuilderWorkbench/BuilderWorkbench.tsx index 978fb7d9..015b4cdc 100644 --- a/website/src/components/BuilderWorkbench/BuilderWorkbench.tsx +++ b/website/src/components/BuilderWorkbench/BuilderWorkbench.tsx @@ -140,6 +140,7 @@ export default function BuilderWorkbench() { const [builderTool, setBuilderTool] = useState("move"); const [selectedShapeId, setSelectedShapeId] = useState(null); const [placingShapeId, setPlacingShapeId] = useState(null); + const [importError, setImportError] = useState(null); const targetMode: TargetMode = "face"; const { @@ -160,6 +161,7 @@ export default function BuilderWorkbench() { } = usePlacements({ meshResolution: sceneOptions.meshResolution, gridResolution: sceneOptions.gridResolution, + onImportError: setImportError, }); // Terrain editor — engaged when toolMode is anything other than "pointer". @@ -283,10 +285,13 @@ export default function BuilderWorkbench() { const source = importedSourceFromFiles(files); if (!source) { - console.warn("[builder] import ignored: choose a .vox, .obj, or .glb file"); + const message = "Choose a .vox, .obj, or .glb file."; + setImportError(message); + console.warn("[builder] import ignored:", message); return; } + setImportError(null); void (async () => { const placement = await buildDroppedPlacement(source, sceneOptions.target[0], sceneOptions.target[1]); if (!placement) return; @@ -295,6 +300,7 @@ export default function BuilderWorkbench() { setSelectedId(snapped.id); setSelectedShapeId(null); setBuilderTool("move"); + setImportError(null); })(); }, [ appendItems, @@ -507,6 +513,12 @@ export default function BuilderWorkbench() { onRemoveItem={handleDeleteItem} selected={selected} /> + {importError ? ( +
+ Import skipped + {importError} +
+ ) : null} diff --git a/website/src/components/BuilderWorkbench/defaults.ts b/website/src/components/BuilderWorkbench/defaults.ts index 894644cd..8468f7ca 100644 --- a/website/src/components/BuilderWorkbench/defaults.ts +++ b/website/src/components/BuilderWorkbench/defaults.ts @@ -66,5 +66,5 @@ export const DEFAULT_SCENE: SceneOptionsState = { fpvRenderDistance: 40, snapToGrid: true, gridResolution: BUILDER_DEFAULT_GRID_RESOLUTION, - gridTone: "gray", + gridTone: "dark", }; diff --git a/website/src/components/BuilderWorkbench/hooks/usePlacements.ts b/website/src/components/BuilderWorkbench/hooks/usePlacements.ts index 7f24b604..0286e5df 100644 --- a/website/src/components/BuilderWorkbench/hooks/usePlacements.ts +++ b/website/src/components/BuilderWorkbench/hooks/usePlacements.ts @@ -12,6 +12,7 @@ import { activeMeshResolution, type WorkbenchMeshResolution } from "../../types" export interface UsePlacementsOptions { meshResolution: WorkbenchMeshResolution; gridResolution: number; + onImportError?: (message: string) => void; } export interface UsePlacementsResult { @@ -44,7 +45,7 @@ export interface UsePlacementsResult { meshHandlesTick: number; } -export function usePlacements({ meshResolution, gridResolution }: UsePlacementsOptions): UsePlacementsResult { +export function usePlacements({ meshResolution, gridResolution, onImportError }: UsePlacementsOptions): UsePlacementsResult { const effectiveMeshResolution = activeMeshResolution(meshResolution); const [placedItems, setPlacedItems] = useState([]); const [selectedId, setSelectedId] = useState(null); @@ -170,11 +171,12 @@ export function usePlacements({ meshResolution, gridResolution }: UsePlacementsO }; } catch (e) { loaded?.dispose(); + onImportError?.(e instanceof Error ? e.message : String(e)); console.error("[builder] failed to import model", source.primaryFile.name, e); return null; } }, - [effectiveMeshResolution, gridResolution], + [effectiveMeshResolution, gridResolution, onImportError], ); const appendItems = useCallback((items: PlacedItem[]) => { diff --git a/website/src/components/DocsHeader.astro b/website/src/components/DocsHeader.astro index a2d067ac..28bc33a7 100644 --- a/website/src/components/DocsHeader.astro +++ b/website/src/components/DocsHeader.astro @@ -34,7 +34,7 @@ const topLinks = [ ]; --- -
+
{!isLanding && (
@@ -212,6 +212,10 @@ const topLinks = [ } @media (max-width: 38rem) { + :global(:root) { + --sl-nav-height: 90px; + } + .header { grid-template-areas: "brand spacer" @@ -221,6 +225,10 @@ const topLinks = [ column-gap: 12px; } + .header--landing { + height: 72px; + } + .title-wrapper { grid-area: brand; padding-left: 0; diff --git a/website/src/components/GalleryWorkbench/GalleryWorkbench.tsx b/website/src/components/GalleryWorkbench/GalleryWorkbench.tsx index da3ed187..1a997aaf 100644 --- a/website/src/components/GalleryWorkbench/GalleryWorkbench.tsx +++ b/website/src/components/GalleryWorkbench/GalleryWorkbench.tsx @@ -155,6 +155,73 @@ const DEFAULT_PARSER: ParserOptionsState = { const LIGHT_HELPER_TILE = 50; const LIGHT_HELPER_SELECTOR = ".dn-light-helper"; +const RESPONSIVE_ZOOM_BREAKPOINT = 900; +const RESPONSIVE_ZOOM_BOTTOM_RESERVE = 72; +const RESPONSIVE_ZOOM_MIN_SCALE = 0.42; + +function clamp(value: number, min: number, max: number): number { + if (!Number.isFinite(value)) return min; + return Math.min(Math.max(value, min), max); +} + +function responsiveZoomScaleForViewport(width: number, height: number): number { + if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { + return 1; + } + const effectiveHeight = Math.max(1, height - RESPONSIVE_ZOOM_BOTTOM_RESERVE); + const widthScale = width < RESPONSIVE_ZOOM_BREAKPOINT + ? width / RESPONSIVE_ZOOM_BREAKPOINT + : 1; + const heightScale = effectiveHeight < RESPONSIVE_ZOOM_BREAKPOINT + ? effectiveHeight / RESPONSIVE_ZOOM_BREAKPOINT + : 1; + return clamp(Math.min(widthScale, heightScale), RESPONSIVE_ZOOM_MIN_SCALE, 1); +} + +function initialResponsiveZoomScale(): number { + if (typeof window === "undefined") return 1; + return responsiveZoomScaleForViewport(window.innerWidth, window.innerHeight); +} + +function useResponsiveViewportZoomScale( + viewportRef: RefObject, +): number { + const [scale, setScale] = useState(initialResponsiveZoomScale); + + useEffect(() => { + const viewport = viewportRef.current; + if (!viewport) return; + + const updateScale = (width: number, height: number): void => { + const next = responsiveZoomScaleForViewport(width, height); + setScale((current) => Math.abs(current - next) < 0.005 ? current : next); + }; + const readScale = (): void => { + const rect = viewport.getBoundingClientRect(); + updateScale(rect.width, rect.height); + }; + + readScale(); + window.addEventListener("resize", readScale); + + let observer: ResizeObserver | null = null; + if (typeof ResizeObserver !== "undefined") { + observer = new ResizeObserver((entries) => { + const rect = entries[0]?.contentRect; + if (rect) updateScale(rect.width, rect.height); + else readScale(); + }); + observer.observe(viewport); + } + + return () => { + window.removeEventListener("resize", readScale); + observer?.disconnect(); + }; + }, [viewportRef]); + + return scale; +} interface ScreenPoint { x: number; @@ -591,6 +658,23 @@ export default function GalleryWorkbench() { }, []); const { handleCameraChange } = useGuiCameraSync({ setSceneOptions }); + const responsiveZoomScale = useResponsiveViewportZoomScale(viewportRef); + const renderSceneOptions = useMemo(() => { + if (responsiveZoomScale === 1) return sceneOptions; + return { + ...sceneOptions, + zoom: sceneOptions.zoom * responsiveZoomScale, + }; + }, [sceneOptions, responsiveZoomScale]); + const handleRenderCameraChange = useCallback( + (camera: { rotX: number; rotY: number; zoom: number; target?: ReactVec3 }) => { + handleCameraChange({ + ...camera, + zoom: camera.zoom / Math.max(responsiveZoomScale, 0.001), + }); + }, + [handleCameraChange, responsiveZoomScale], + ); const dropped = useDroppedFiles({ onDroppedSource: (source) => { @@ -796,7 +880,7 @@ export default function GalleryWorkbench() { reactAnimatedPolygons: animation.reactAnimatedPolygons, interiorFill: sceneOptions.interiorFill, }); - useLightRotationDrag(viewportRef, sceneOptions, helperScale, gizmoDragging, updateScene); + useLightRotationDrag(viewportRef, renderSceneOptions, helperScale, gizmoDragging, updateScene); const renderModelPolygons = useMemo( () => sceneOptions.solidMaterials ? withSolidMaterials(modelPolygons, parserOptions.defaultColor) @@ -1080,7 +1164,7 @@ export default function GalleryWorkbench() { polygons={renderModelPolygons} interiorShellPolygons={interiorShellPolygons} parseResult={renderLoaded?.parseResult} - options={sceneOptions} + options={renderSceneOptions} directionalLight={directionalLight} ambientLight={ambientLight} showAxes={sceneOptions.showAxes} @@ -1094,7 +1178,7 @@ export default function GalleryWorkbench() { animationDurationSeconds={activeAnimation?.duration} animationFrameFactory={vanillaAnimationFrameFactory} onBuild={setVanillaBuildMs} - onCameraChange={handleCameraChange} + onCameraChange={handleRenderCameraChange} enableSelection={sceneOptions.selection} meshId={renderLoaded?.label ?? "model"} onSelectionChange={setVanillaSelectedIds} @@ -1106,7 +1190,7 @@ export default function GalleryWorkbench() { ) : ( )} + {loadError ? ( +
+ Import skipped + {loadError} +
+ ) : null}
@@ -1140,7 +1230,6 @@ export default function GalleryWorkbench() { id="gallery-controls-panel" className={mobilePanel === "controls" ? "is-mobile-open" : ""} loading={loading} - loadError={loadError} > = { "urban/ATM.glb": polyPizzaCityPackAttribution("J-Toastie", "Creative Commons Attribution"), "urban/Planter & Bushes.glb": polyPizzaCityPackAttribution("J-Toastie", "Creative Commons Attribution"), "urban/Man.glb": polyPizzaCityPackAttribution("Quaternius", "CC0 1.0"), - "urban/Adventurer.glb": polyPizzaCityPackAttribution("Quaternius", "CC0 1.0"), "urban/Animated Woman.glb": polyPizzaCityPackAttribution("Quaternius", "CC0 1.0"), }; diff --git a/website/src/components/GalleryWorkbench/presets/buckets.ts b/website/src/components/GalleryWorkbench/presets/buckets.ts index 3707470f..d1343765 100644 --- a/website/src/components/GalleryWorkbench/presets/buckets.ts +++ b/website/src/components/GalleryWorkbench/presets/buckets.ts @@ -7,11 +7,14 @@ export const ANIMATED_PRESET_IDS = new Set([ "glb-poly-pizza-llama", "glb-poly-pizza-man", "glb-poly-pizza-pug", - "glb-poly-pizza-rabbit-blond", "glb-poly-pizza-sheep", ]); -export function isAnimatedPreset(preset: Pick): boolean { +export function isAnimatedPreset( + preset: Pick, +): boolean { + if (preset.galleryBucket) return preset.galleryBucket === "Animated"; + return ( ANIMATED_PRESET_IDS.has(preset.id) || preset.category === "Animated" || @@ -22,9 +25,10 @@ export function isAnimatedPreset(preset: Pick - +