beam: add local converter and STEP viewer flow
This commit is contained in:
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user