Skip to content
Merged
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
1 change: 0 additions & 1 deletion bench/notes/BENCH.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion bench/notes/PERF_INVESTIGATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 0 additions & 5 deletions bench/perf-shared.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
9 changes: 9 additions & 0 deletions website/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed website/public/gallery/glb/urban/Adventurer.glb
Binary file not shown.
2 changes: 0 additions & 2 deletions website/public/gallery/vox-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
Binary file removed website/public/gallery/vox/Garden.vox
Binary file not shown.
Binary file removed website/public/gallery/vox/desert.vox
Binary file not shown.
Binary file added website/public/polycss-github.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions website/public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
User-agent: *
Allow: /

Sitemap: https://polycss.com/sitemap-index.xml
14 changes: 13 additions & 1 deletion website/src/components/BuilderWorkbench/BuilderWorkbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export default function BuilderWorkbench() {
const [builderTool, setBuilderTool] = useState<BuilderToolMode>("move");
const [selectedShapeId, setSelectedShapeId] = useState<string | null>(null);
const [placingShapeId, setPlacingShapeId] = useState<string | null>(null);
const [importError, setImportError] = useState<string | null>(null);
const targetMode: TargetMode = "face";

const {
Expand All @@ -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".
Expand Down Expand Up @@ -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;
Expand All @@ -295,6 +300,7 @@ export default function BuilderWorkbench() {
setSelectedId(snapped.id);
setSelectedShapeId(null);
setBuilderTool("move");
setImportError(null);
})();
}, [
appendItems,
Expand Down Expand Up @@ -507,6 +513,12 @@ export default function BuilderWorkbench() {
onRemoveItem={handleDeleteItem}
selected={selected}
/>
{importError ? (
<div className="dn-viewport-notice dn-viewport-notice--error" role="alert">
<span className="dn-viewport-notice__title">Import skipped</span>
<span className="dn-viewport-notice__body">{importError}</span>
</div>
) : null}
<BuilderCameraModePill dragMode={sceneOptions.dragMode} updateScene={updateScene} />
</div>
</main>
Expand Down
2 changes: 1 addition & 1 deletion website/src/components/BuilderWorkbench/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,5 @@ export const DEFAULT_SCENE: SceneOptionsState = {
fpvRenderDistance: 40,
snapToGrid: true,
gridResolution: BUILDER_DEFAULT_GRID_RESOLUTION,
gridTone: "gray",
gridTone: "dark",
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { activeMeshResolution, type WorkbenchMeshResolution } from "../../types"
export interface UsePlacementsOptions {
meshResolution: WorkbenchMeshResolution;
gridResolution: number;
onImportError?: (message: string) => void;
}

export interface UsePlacementsResult {
Expand Down Expand Up @@ -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<PlacedItem[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
Expand Down Expand Up @@ -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[]) => {
Expand Down
10 changes: 9 additions & 1 deletion website/src/components/DocsHeader.astro
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const topLinks = [
];
---

<div class="header">
<div class:list={["header", { "header--landing": isLanding }]}>
{!isLanding && (
<div class="title-wrapper sl-flex">
<SiteTitle />
Expand Down Expand Up @@ -212,6 +212,10 @@ const topLinks = [
}

@media (max-width: 38rem) {
:global(:root) {
--sl-nav-height: 90px;
}

.header {
grid-template-areas:
"brand spacer"
Expand All @@ -221,6 +225,10 @@ const topLinks = [
column-gap: 12px;
}

.header--landing {
height: 72px;
}

.title-wrapper {
grid-area: brand;
padding-left: 0;
Expand Down
101 changes: 95 additions & 6 deletions website/src/components/GalleryWorkbench/GalleryWorkbench.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement | null>,
): 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;
Expand Down Expand Up @@ -591,6 +658,23 @@ export default function GalleryWorkbench() {
}, []);

const { handleCameraChange } = useGuiCameraSync({ setSceneOptions });
const responsiveZoomScale = useResponsiveViewportZoomScale(viewportRef);
const renderSceneOptions = useMemo<SceneOptionsState>(() => {
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) => {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand All @@ -1106,15 +1190,15 @@ export default function GalleryWorkbench() {
) : (
<ReactScene
rendererDebugKey={rendererDebugKey}
sceneOptions={sceneOptions}
sceneOptions={renderSceneOptions}
scenePolygons={renderModelPolygons}
interiorShellPolygons={interiorShellPolygons}
directionalLight={directionalLight}
ambientLight={ambientLight}
textureQuality={textureQuality}
gizmoDragging={gizmoDragging}
setGizmoDragging={setGizmoDragging}
handleCameraChange={handleCameraChange}
handleCameraChange={handleRenderCameraChange}
loaded={loaded}
selectedMeshes={selectedMeshes}
setSelectedMeshes={setSelectedMeshes}
Expand All @@ -1130,6 +1214,12 @@ export default function GalleryWorkbench() {
helperTarget={helperTarget}
/>
)}
{loadError ? (
<div className="dn-viewport-notice dn-viewport-notice--error" role="alert">
<span className="dn-viewport-notice__title">Import skipped</span>
<span className="dn-viewport-notice__body">{loadError}</span>
</div>
) : null}
</div>
<DropOverlay active={dropped.dropActive} />
</main>
Expand All @@ -1140,7 +1230,6 @@ export default function GalleryWorkbench() {
id="gallery-controls-panel"
className={mobilePanel === "controls" ? "is-mobile-open" : ""}
loading={loading}
loadError={loadError}
>
<DockModel
metrics={metrics}
Expand Down
34 changes: 34 additions & 0 deletions website/src/components/GalleryWorkbench/gallery-workbench.css
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,40 @@
background: #000;
}

.dn-viewport-notice {
position: absolute;
left: 50%;
top: 50%;
z-index: 24;
width: min(520px, calc(100% - 48px));
max-width: calc(100vw - 48px);
box-sizing: border-box;
transform: translate(-50%, -50%);
display: grid;
gap: 8px;
padding: 14px 16px;
border-radius: 8px;
border: 1px solid rgba(248, 113, 113, 0.42);
background: rgba(16, 18, 24, 0.94);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.42);
color: #f8fafc;
pointer-events: none;
}

.dn-viewport-notice__title {
color: #fecaca;
font-size: 13px;
font-weight: 760;
line-height: 1.2;
}

.dn-viewport-notice__body {
color: #fca5a5;
font-size: 13px;
line-height: 1.45;
overflow-wrap: anywhere;
}

.dn-light-helper,
.dn-light-helper * {
cursor: grab;
Expand Down
Loading
Loading