beam: add local converter and STEP viewer flow

This commit is contained in:
CODEX
2026-06-04 19:39:22 +03:00
parent 9aaf82862f
commit 254bec97ab
10 changed files with 625 additions and 10 deletions
+61
View File
@@ -73,6 +73,21 @@ body {
overflow: hidden;
}
html,
body,
.viewer-wrapper,
#viewerCanvas {
outline: none;
}
#viewerCanvas:focus,
#viewerCanvas:focus-visible,
.viewer-wrapper:focus,
.viewer-wrapper:focus-visible {
outline: none;
box-shadow: none;
}
header {
position: fixed;
top: 10px;
@@ -576,6 +591,52 @@ header {
margin-right: 6px;
}
.display-mode-panel {
margin-top: 10px;
}
.display-mode-title {
margin-bottom: 7px;
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.display-mode-buttons {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
}
.display-mode-buttons button {
min-width: 0;
border: 1px solid var(--modal-border);
border-radius: 9px;
background: var(--modal-focus);
color: var(--muted);
cursor: pointer;
font: inherit;
font-size: 11px;
font-weight: 700;
line-height: 1;
outline: none;
padding: 8px 6px;
transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.display-mode-buttons button:hover,
.display-mode-buttons button.active {
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 14%, var(--modal-focus));
color: var(--text);
}
.display-mode-buttons button:focus,
.display-mode-buttons button:focus-visible {
outline: none;
box-shadow: none;
}
.examples-bar {
position: fixed;
left: 0;
+119 -8
View File
@@ -35,6 +35,8 @@ const settingsButton = document.querySelector('[data-action="settings"]');
const templatesButton = document.querySelector('[data-action="templates"]');
const projectionToggle = document.querySelector('[data-action="projection"]');
const treeContainer = document.getElementById("treeContainer");
const displayModeControl = document.getElementById("displayModeControl");
const displayModeButtons = document.querySelectorAll("[data-display-mode]");
const logList = document.getElementById("logList");
const loaderEl = document.getElementById("loader");
const navCubeCanvas = document.getElementById("navCube");
@@ -162,6 +164,7 @@ let gizmo = null;
const transformStore = {}; // modelId -> objectId -> {position, rotation, scale}
const modelTypes = {}; // modelId -> normalized type
const sceneModels = {}; // modelId -> SceneModel
let activeDisplayMode = "source";
let currentSelection = null; // { modelId, objectId }
const customTrees = new Map(); // modelId -> element
let navCubePlugin = null;
@@ -229,6 +232,52 @@ const updateProjectionButton = () => {
}
};
const setDisplayModeButtonState = () => {
displayModeButtons.forEach((button) => {
button.classList.toggle("active", button.dataset.displayMode === activeDisplayMode);
});
};
const getDisplayModeColorize = (mode) => {
switch (mode) {
case "white":
return [1, 1, 1];
case "contrast":
return [0.78, 0.82, 1];
case "source":
default:
return null;
}
};
const applyDisplayModeToModel = (modelId) => {
const model = sceneModels[modelId] || viewer?.scene?.models?.[modelId];
if (!model) return;
const colorize = getDisplayModeColorize(activeDisplayMode);
const objects = Object.values(model.objects || {});
const targets = objects.length ? objects : [model];
targets.forEach((entity) => {
if (!entity) return;
try {
entity.colorize = colorize;
entity.opacity = 1;
if (activeDisplayMode !== "source") {
entity.edges = false;
}
} catch (err) {
console.warn("display mode update failed", err);
}
});
viewer?.scene?.glRedraw?.();
};
const applyDisplayMode = () => {
setDisplayModeButtonState();
Object.keys(sceneModels).forEach((modelId) => applyDisplayModeToModel(modelId));
};
const toggleProjection = () => {
if (!viewer?.camera) return;
const cam = viewer.camera;
@@ -658,12 +707,23 @@ const applyGlobalTarget = () => {
});
});
window.addEventListener("focus", () => {
const scheduleCameraRearm = () => {
rearmCameraControl();
});
window.setTimeout(rearmCameraControl, 60);
window.setTimeout(rearmCameraControl, 220);
};
window.addEventListener("pointerup", () => {
rearmCameraControl();
window.addEventListener("blur", scheduleCameraRearm);
window.addEventListener("focus", scheduleCameraRearm);
window.addEventListener("pageshow", scheduleCameraRearm);
window.addEventListener("pointerup", scheduleCameraRearm, true);
window.addEventListener("mouseup", scheduleCameraRearm, true);
window.addEventListener("touchend", scheduleCameraRearm, true);
window.addEventListener("touchcancel", scheduleCameraRearm, true);
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
scheduleCameraRearm();
}
});
const makeTranslationMat = (x, y, z) => {
@@ -1829,6 +1889,7 @@ const buildProjectSnapshot = async (name) => {
transparent: !!toggleTransparent?.checked,
edges: !!toggleEdges?.checked,
addMode: !!addMode?.checked,
displayMode: activeDisplayMode,
selection: {
ids: lastSelection.ids || [],
modelId: lastSelection.modelId || null,
@@ -1889,6 +1950,10 @@ const applyViewerState = (state) => {
if (typeof state.addMode === "boolean" && addMode) {
addMode.checked = state.addMode;
}
if (typeof state.displayMode === "string") {
activeDisplayMode = state.displayMode;
applyDisplayMode();
}
if (state.transforms && typeof state.transforms === "object") {
Object.keys(transformStore).forEach((k) => delete transformStore[k]);
Object.assign(transformStore, safeClone(state.transforms));
@@ -2067,8 +2132,8 @@ const initViewer = async () => {
if (mainCanvas) {
mainCanvas.addEventListener("pointerdown", () => {
focusCanvasAndControls();
resetCameraControl();
});
}, true);
mainCanvas.addEventListener("pointerenter", scheduleCameraRearm);
}
rebuildNavCube(sdk);
@@ -2296,7 +2361,7 @@ const initViewer = async () => {
const loadModel = (options) => {
try {
const { type, url, name = "", replace = true, id: forcedId, meta } = options;
const { type, url, name = "", replace = true, id: forcedId, meta, edges } = options;
const normalizedType = normalizeType(type, typeof url === "string" ? url : "");
const inferredType = normalizedType || normalizeType(guessTypeFromName(typeof url === "string" ? url : ""));
if (!inferredType || !acceptByType[inferredType]) {
@@ -2337,7 +2402,11 @@ const initViewer = async () => {
blobUrls.push(blobUrl);
srcToLoad = blobUrl;
}
const loadOpts = { id, src: srcToLoad, edges: toggleEdges.checked };
const loadOpts = {
id,
src: srcToLoad,
edges: edges ?? (inferredType === "gltf" ? false : toggleEdges.checked),
};
if (inferredType === "gltf") loadOpts.autoMetaModel = true;
if (inferredType === "bim" || inferredType === "las") loadOpts.rotation = [-90, 0, 0];
if (inferredType === "stl") loadOpts.smoothNormals = true;
@@ -2355,6 +2424,7 @@ const initViewer = async () => {
setStatus(`Загружено: ${name || inferredType}`);
dropZone.classList.add("hidden");
setLoading(false);
applyDisplayModeToModel(id);
updateNavCubeVisibility();
updatePanelsVisibility();
rearmCameraControl();
@@ -2414,6 +2484,35 @@ const initViewer = async () => {
model.on("error", (err) => reject(new Error(err?.message || err)));
});
const loadStartupModelFromQuery = () => {
const params = new URLSearchParams(window.location.search);
const src = params.get("url") || params.get("src");
if (!src) return;
const name = params.get("name") || src.split("/").pop() || "model";
const type = normalizeType(params.get("type"), src) || normalizeType(guessTypeFromName(name));
if (!type) {
showError("Не удалось определить формат модели из URL");
return;
}
if (params.has("displayMode")) {
activeDisplayMode = params.get("displayMode") || "source";
setDisplayModeButtonState();
}
loadModel({
type,
url: src,
name,
replace: params.get("replace") !== "false",
edges: params.has("edges") ? params.get("edges") !== "false" : undefined,
meta: {
label: name,
source: "query",
},
});
};
const loadProjectSnapshot = async (project) => {
if (!project) {
throw new Error("Нет данных проекта");
@@ -2698,6 +2797,16 @@ const loadProjectSnapshot = async (project) => {
rebuildNavCube(sdk);
applyHighlightTheme();
applyMeasureTheme();
if (displayModeControl) {
displayModeButtons.forEach((button) => {
button.addEventListener("click", () => {
activeDisplayMode = button.dataset.displayMode || "source";
applyDisplayMode();
focusCanvasAndControls();
});
});
setDisplayModeButtonState();
}
loadProjectsList().catch(() => {
templatesMenu?.setData({ templates: templateFallbacks, projects: [], error: "API недоступно" });
});
@@ -3076,6 +3185,8 @@ const loadProjectSnapshot = async (project) => {
onClose: () => {}
});
}
loadStartupModelFromQuery();
};
document.addEventListener("DOMContentLoaded", () => {
+10 -2
View File
@@ -87,7 +87,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="./dcViewer.css?v=3">
<link rel="stylesheet" href="./dcViewer.css?v=4">
</head>
<body>
<header>
@@ -128,6 +128,14 @@
<div class="panel-section">
<h3>Инспектор</h3>
<div id="treeContainer" class="tree"></div>
<div class="display-mode-panel" id="displayModeControl">
<div class="display-mode-title">Режим отображения</div>
<div class="display-mode-buttons">
<button type="button" data-display-mode="source" class="active">Исходный</button>
<button type="button" data-display-mode="white">Белый</button>
<button type="button" data-display-mode="contrast">Контраст</button>
</div>
</div>
</div>
<div class="panel-section" id="projectNameSection">
<h3>Название проекта</h3>
@@ -223,6 +231,6 @@
<input id="fileInput" class="hidden" type="file"
accept=".xkt,.glb,.gltf,.bim,.las,.laz,.obj,.stl">
<script type="module" src="./dcViewer.js"></script>
<script type="module" src="./dcViewer.js?v=4"></script>
</body>
</html>