fix: resolve workspace integration blockers (#5)

This commit is contained in:
2026-07-29 10:01:17 +09:00
parent 3279ac099e
commit cadbd5fb60
37 changed files with 1817 additions and 73 deletions
+9 -3
View File
@@ -1,5 +1,4 @@
// Types for the ported JS 2D engine (Viewer2D.js). Colocated so tsc resolves
// `./Viewer2D.js` imports without allowJs. Only the methods the app uses are typed.
// 포팅된 JavaScript 2D 엔진의 공개 interface입니다.
export class Viewer2D {
constructor(container: HTMLElement);
/** Render a DWG/DXF parseResult. `keepView` preserves pan/zoom on re-render. */
@@ -9,12 +8,19 @@ export class Viewer2D {
/** Resync renderer/camera to the container size (call after un-hiding). */
resize(): void;
onSelect(cb: (entity: unknown) => void): void;
setZoomSpeed(speed: number): void;
getZoomSpeed(): number;
setTheme(dark: boolean): void;
setGrid(visible: boolean): void;
getLayerInfo(): { name: string; colorHex: string; count: number; visible: boolean }[];
setHiddenLayers(nameSet: Set<string>): void;
getZoomPercent(): number;
onViewChange(cb: () => void): void;
onViewChange(cb: () => void): () => void;
getStats(): { entities: number; blocks: number };
snapshotWebPBlob(): Promise<Blob | null>;
startMeasure(cb: (r: { phase: string; ax: number; ay: number; az: number; bx: number; by: number; bz: number; d: number }) => void): void;
stopMeasure(): void;
setAccent(hex: string): void;
/** RAF, DOM listener, WebGL 자원과 canvas를 멱등으로 해제합니다. */
dispose(): void;
}
+118 -38
View File
@@ -16,7 +16,7 @@ const ARC_SEGS = 64;
const ELLIPSE_SEGS = 72;
const CLICK_THRESHOLD_PX = 14;
function stripMText(s) {
function stripMText(s) {
if (!s) return '';
return s
.replace(/\\A\d+;/g, '') // \A1; vertical alignment
@@ -30,11 +30,28 @@ function stripMText(s) {
.replace(/^[ \t]+|[ \t]+$/gm, '') // trim each line's edges (keeps interior \n)
.replace(/\n{3,}/g, '\n\n')
.replace(/^\n+|\n+$/g, ''); // drop leading/trailing blank lines
}
export class Viewer2D {
constructor(container) {
this._container = container;
}
function disposeMaterial(material) {
for (const item of (Array.isArray(material) ? material : [material])) {
item?.map?.dispose();
item?.dispose();
}
}
function disposeObject(root) {
root?.traverse?.((object) => {
object.geometry?.dispose();
if (object.material) disposeMaterial(object.material);
});
}
export class Viewer2D {
constructor(container) {
this._disposed = false;
this._rafId = null;
this._viewChangeCallbacks = new Set();
this._container = container;
this._entityMeta = [];
this._layerByHandle = new Map();
this._layerByName = new Map();
@@ -74,26 +91,43 @@ export class Viewer2D {
}
}
window.addEventListener('keydown', (e) => { if (e.key === 'f' || e.key === 'F') this.fit(); });
this._handleKeyDown = (e) => {
if (e.key === 'f' || e.key === 'F') this.fit();
};
window.addEventListener('keydown', this._handleKeyDown);
this._group = new THREE.Group();
this._scene.add(this._group);
window.addEventListener('resize', () => this._onResize());
// Prevent browser auto-scroll popup when middle-button is pressed on the WebGL canvas
this._renderer.domElement.addEventListener('pointerdown', (e) => {
if (e.button === 1) e.preventDefault();
});
this._renderer.domElement.addEventListener('mousedown', (e) => {
this._downXY = [e.clientX, e.clientY];
if (e.button === 1) e.preventDefault();
});
this._renderer.domElement.addEventListener('click', (e) => this._onClick(e));
this._handleResize = () => this._onResize();
window.addEventListener('resize', this._handleResize);
// Prevent browser auto-scroll popup when middle-button is pressed on the WebGL canvas
this._handlePointerDown = (e) => {
if (e.button === 1) e.preventDefault();
};
this._handleMouseDown = (e) => {
this._downXY = [e.clientX, e.clientY];
if (e.button === 1) e.preventDefault();
};
this._handleClick = (e) => this._onClick(e);
this._renderer.domElement.addEventListener('pointerdown', this._handlePointerDown);
this._renderer.domElement.addEventListener('mousedown', this._handleMouseDown);
this._renderer.domElement.addEventListener('click', this._handleClick);
this._selColor = { r: 0xd8 / 255, g: 0x3a / 255, b: 0x2f / 255 };
this._gridVisible = false;
this._gridMesh = null;
this._controls.addEventListener('change', () => { if (this._gridVisible) this._rebuildGrid(); });
this._animate();
this._handleControlsChange = () => {
if (this._gridVisible) this._rebuildGrid();
for (const callback of this._viewChangeCallbacks) callback();
};
this._controls.addEventListener('change', this._handleControlsChange);
this._onAnimationFrame = () => {
if (this._disposed) return;
this._controls.update();
this._renderer.render(this._scene, this._camera);
this._rafId = requestAnimationFrame(this._onAnimationFrame);
};
this._animate();
}
onSelect(cb) { this._onSelectCb = cb; }
@@ -683,7 +717,11 @@ export class Viewer2D {
getZoomPercent() { return Math.round((this._camera?.zoom ?? 1) * 100); }
/** Subscribe to pan/zoom changes (for live zoom readout). */
onViewChange(cb) { this._controls?.addEventListener('change', cb); }
onViewChange(cb) {
if (this._disposed) return () => {};
this._viewChangeCallbacks.add(cb);
return () => this._viewChangeCallbacks.delete(cb);
}
/** Hide entities on the given layer names (Set<string>); re-renders keeping the view. */
setHiddenLayers(nameSet) {
@@ -2328,7 +2366,7 @@ export class Viewer2D {
return Infinity;
}
_fit(box) {
_fit(box) {
const c = box.getCenter(new THREE.Vector3());
const size = box.getSize(new THREE.Vector3());
const w = this._container.clientWidth || 800;
@@ -2346,15 +2384,57 @@ export class Viewer2D {
this._camera.up.set(-Math.sin(a), Math.cos(a), 0);
this._camera.updateProjectionMatrix();
this._controls.target.set(c.x, c.y, 0);
this._controls.update();
}
_clear() {
for (let i = this._group.children.length - 1; i >= 0; i--) {
const o = this._group.children[i];
o.geometry?.dispose();
if (o.material) { o.material.map?.dispose(); o.material.dispose(); }
this._group.remove(o);
this._controls.update();
}
dispose() {
if (this._disposed) return;
this._disposed = true;
if (this._rafId !== null) {
cancelAnimationFrame(this._rafId);
this._rafId = null;
}
window.removeEventListener('keydown', this._handleKeyDown);
window.removeEventListener('resize', this._handleResize);
const canvas = this._renderer.domElement;
canvas.removeEventListener('pointerdown', this._handlePointerDown);
canvas.removeEventListener('mousedown', this._handleMouseDown);
canvas.removeEventListener('click', this._handleClick);
this._controls.removeEventListener('change', this._handleControlsChange);
this._viewChangeCallbacks.clear();
this.stopMeasure();
this.setGrid(false);
this._clear();
this._scene.remove(this._group);
this._controls.dispose();
this._renderer.dispose();
this._renderer.forceContextLoss();
canvas.remove();
this._scene.clear();
this._lastResult = null;
this._onSelectCb = null;
this._measureCb = null;
this._entityMeta = [];
this._pickSegs = [];
this._pickSegMeta = [];
this._pickFills = [];
this._layerByHandle.clear();
this._layerByName.clear();
this._layerNameByHandle?.clear();
this._linetypeByName?.clear();
this._hiddenLayers?.clear();
this._texCache?.clear();
}
_clear() {
for (let i = this._group.children.length - 1; i >= 0; i--) {
const o = this._group.children[i];
disposeObject(o);
this._group.remove(o);
}
// Shared text textures were disposed via the sprites above (re-dispose is a
// no-op); drop the cache so the next load rasterizes fresh.
@@ -2362,8 +2442,9 @@ export class Viewer2D {
this._textGen++; // invalidate any in-flight async text flush
}
_onResize() {
const w = this._container.clientWidth, h = this._container.clientHeight;
_onResize() {
if (this._disposed) return;
const w = this._container.clientWidth, h = this._container.clientHeight;
if (!w || !h) return;
this._renderer.setSize(w, h);
// Frustum planes are CAMERA-LOCAL (relative to camera.position), not world coords.
@@ -2375,9 +2456,8 @@ export class Viewer2D {
this._camera.updateProjectionMatrix();
}
_animate() {
requestAnimationFrame(() => this._animate());
this._controls.update();
this._renderer.render(this._scene, this._camera);
}
}
_animate() {
if (this._disposed || this._rafId !== null) return;
this._rafId = requestAnimationFrame(this._onAnimationFrame);
}
}
@@ -3,7 +3,7 @@
"type": "module",
"description": "WASM wrapper around acadrust (MPL-2.0, used unmodified) emitting the hmwebviewer parseResult shape",
"version": "0.1.0",
"license": "MIT",
"license": "MPL-2.0",
"files": [
"acadrust_dwg_bg.wasm",
"acadrust_dwg.js",