Merge branch 'main' into feature/initial-viewer-ui-updates

This commit is contained in:
Mike Tasset
2025-07-30 21:01:09 +02:00
7 changed files with 274 additions and 41 deletions
+25 -29
View File
@@ -5,44 +5,40 @@ description = "Speckle IFC importer worker app"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"asyncpg>=0.30.0",
"typed-settings>=24.5.0",
"pydantic>=2.11.7",
"python-dotenv>=1.0.0",
"structlog>=25.4.0",
"structlog-to-seq>=21.0.0",
"specklepy[speckleifc]>=3.0.4.dev8",
"asyncpg>=0.30.0",
"typed-settings>=24.5.0",
"pydantic>=2.11.7",
"python-dotenv>=1.0.0",
"structlog>=25.4.0",
"structlog-to-seq>=21.0.0",
"specklepy[speckleifc]>=3.0.4.dev8",
"colorful>=0.5.7",
]
[dependency-groups]
dev = [
"asyncpg-stubs>=0.30.2",
"colorama>=0.4.6",
"colorful>=0.5.7",
"ruff>=0.12.2",
]
dev = ["asyncpg-stubs>=0.30.2", "colorama>=0.4.6", "ruff>=0.12.2"]
[tool.ruff]
exclude = [".venv", "**/*.yml"]
[tool.ruff.lint]
select = [
"A",
# pycodestyle
"E",
# Pyflakes
"F",
# pyupgrade
"UP",
# flake8-bugbear
"B",
# flake8-simplify
"SIM",
# isort
"I",
# PEP8 naming
"N",
"ASYNC",
"A",
# pycodestyle
"E",
# Pyflakes
"F",
# pyupgrade
"UP",
# flake8-bugbear
"B",
# flake8-simplify
"SIM",
# isort
"I",
# PEP8 naming
"N",
"ASYNC",
]
[build-system]
+2 -2
View File
@@ -251,6 +251,7 @@ version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "asyncpg" },
{ name = "colorful" },
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "specklepy", extra = ["speckleifc"] },
@@ -263,13 +264,13 @@ dependencies = [
dev = [
{ name = "asyncpg-stubs" },
{ name = "colorama" },
{ name = "colorful" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "asyncpg", specifier = ">=0.30.0" },
{ name = "colorful", specifier = ">=0.5.7" },
{ name = "pydantic", specifier = ">=2.11.7" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "specklepy", extras = ["speckleifc"], specifier = ">=3.0.4.dev8" },
@@ -282,7 +283,6 @@ requires-dist = [
dev = [
{ name = "asyncpg-stubs", specifier = ">=0.30.2" },
{ name = "colorama", specifier = ">=0.4.6" },
{ name = "colorful", specifier = ">=0.5.7" },
{ name = "ruff", specifier = ">=0.12.2" },
]
@@ -431,8 +431,13 @@ describe('Core GraphQL Subscriptions (New)', () => {
onUserStreamAdded.waitForMessage()
])
expect(onUserProjectsUpdated.getMessages()).to.have.lengthOf(2)
expect(onUserStreamAdded.getMessages()).to.have.lengthOf(2)
const projectSubs = onUserProjectsUpdated.getMessages()
expect(projectSubs.length).to.be.gte(1)
expect(projectSubs.length).to.be.lte(2)
const userSubs = onUserStreamAdded.getMessages()
expect(userSubs.length).to.be.gte(1)
expect(userSubs.length).to.be.lte(2)
})
it('should notify me of a project ive just been added to (userProjectsUpdated/userStreamAdded)', async () => {
@@ -20,7 +20,8 @@ import {
DynamicDrawUsage,
Color,
MeshBasicMaterial,
PlaneGeometry
PlaneGeometry,
Euler
} from 'three'
import { intersectObjectWithRay, TransformControls } from '../TransformControls.js'
import { OBB } from 'three/examples/jsm/math/OBB.js'
@@ -39,6 +40,7 @@ import SpeckleLineMaterial from '../../materials/SpeckleLineMaterial.js'
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js'
import SpeckleStandardMaterial from '../../materials/SpeckleStandardMaterial.js'
import { Extension } from '../Extension.js'
import { SectionOutlines } from './SectionOutlines.js'
export enum SectionToolEvent {
DragStart = 'section-box-drag-start',
@@ -56,6 +58,8 @@ export interface SectionToolEventPayload {
const _matrix4 = new Matrix4()
const _quaternion = new Quaternion()
const _vector3 = new Vector3()
const _tempEuler = new Euler()
const _tempQuaternion = new Quaternion()
const unitCube = [
-1 * 0.5,
@@ -133,6 +137,14 @@ export class SectionTool extends Extension {
return [CameraController]
}
/** Configurable rotation snap angle in radians. Set to null to disable snapping */
public rotationSnapAngle: number | null = Math.PI / 12 // 15 degrees by default
/** Note: Rotation snapping only applies to mouse interactions via TransformControls.
* Programmatic calls to setBox() will not apply rotation snapping.
* For complete snapping support, programmatic rotations will need to be handled separately.
*/
/** This is our data model. All we need is an OBB */
protected obb: OBB = new OBB()
@@ -199,6 +211,12 @@ export class SectionTool extends Extension {
/** Hit testing related */
protected raycaster: Raycaster
protected dragging = false
protected shiftKeyPressed = false
protected keydownHandler: (e: KeyboardEvent) => void
protected keyupHandler: (e: KeyboardEvent) => void
protected sectionBoxHistory: OBB[] = []
protected currentHistoryIndex = 0
protected maxHistorySize = 100
/** Manadatory property for all extensions */
public get enabled() {
@@ -317,6 +335,9 @@ export class SectionTool extends Extension {
/** Hook up to le click */
this.viewer.getRenderer().input.on(InputEvent.Click, this.clickHandler.bind(this))
/** Add keyboard event listeners for shift key rotation snapping */
this.setupKeyboardListeners()
/** Start off disabled */
this.enabled = false
}
@@ -514,6 +535,92 @@ export class SectionTool extends Extension {
)
}
/**
* Creates an OBB state from the current OBB
*/
protected createObbState(): OBB {
return new OBB().copy(this.obb)
}
/**
* Applies an OBB state to the current OBB
*/
protected applyObbState(state: OBB): void {
this.obb.copy(state)
}
/**
* Saves the current section box state to history
*/
protected saveToHistory(): void {
const currentState = this.createObbState()
/** If we're not at the latest state and make a new change, remove all future states */
if (
this.currentHistoryIndex < this.sectionBoxHistory.length - 1 &&
this.sectionBoxHistory.length > 1
) {
/** Keep the initial state and all states up to the current position */
this.sectionBoxHistory = this.sectionBoxHistory.slice(
0,
this.currentHistoryIndex + 1
)
}
/** Add current state to history */
this.sectionBoxHistory.push(currentState)
this.currentHistoryIndex = this.sectionBoxHistory.length - 1
/** Remove oldest states if we exceed the history limit */
if (this.sectionBoxHistory.length > this.maxHistorySize) {
this.sectionBoxHistory.shift()
this.currentHistoryIndex = Math.max(0, this.currentHistoryIndex - 1)
}
}
/**
* Sets up keyboard event listeners for shift key rotation snapping and undo/redo
*/
protected setupKeyboardListeners() {
/** Store shift state for use in changeHandler */
this.shiftKeyPressed = false
/** Store references to event listeners for cleanup */
this.keydownHandler = (e: KeyboardEvent) => {
if (e.shiftKey && !this.shiftKeyPressed) {
this.shiftKeyPressed = true
}
/** Handle Cmd/Ctrl+Z for section box undo */
if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) {
/** Only allow undo/redo when section box controls are visible */
if (this.enabled && this.visible) {
e.preventDefault()
this.undoSectionBox()
}
}
/** Handle Cmd/Ctrl+Shift+Z for section box redo */
if ((e.metaKey || e.ctrlKey) && e.key === 'z' && e.shiftKey) {
/** Only allow undo/redo when section box controls are visible */
if (this.enabled && this.visible) {
e.preventDefault()
this.redoSectionBox()
}
}
}
this.keyupHandler = (e: KeyboardEvent) => {
if (!e.shiftKey && this.shiftKeyPressed) {
this.shiftKeyPressed = false
}
}
/** Event listeners */
document.addEventListener('keydown', this.keydownHandler)
document.addEventListener('keyup', this.keyupHandler)
}
/**
* Controls, outline and hitbox update based on the OBB model
*/
@@ -541,18 +648,27 @@ export class SectionTool extends Extension {
}
/**
* Triggers when dragging starts/stops
* Triggers when transform interactions start/stop
* @param event Controls event
*/
//@ts-ignore
protected draggingHandler(event) {
this.dragging = event.value
if (this.dragging) {
/** Save initial state when interaction starts (if this is the first change) */
if (this.sectionBoxHistory.length === 0) {
this.sectionBoxHistory.push(this.createObbState())
this.currentHistoryIndex = 0
}
this.cameraProvider.enabled = false
if (event.target === this.translateControls) this.rotateControls.detach()
else if (event.target === this.rotateControls) this.translateControls.detach()
this.emit(SectionToolEvent.DragStart)
} else {
/** Save final state when interaction ends */
this.saveToHistory()
this.cameraProvider.enabled = true
if (event.target === this.translateControls)
this.rotateControls.attach(this.translationRotationAnchor)
@@ -572,14 +688,19 @@ export class SectionTool extends Extension {
*/
//@ts-ignore
protected changeHandler() {
/** Just copy over position, rotation and scale*/
/** Just copy over position, rotation and scale*/
this.obb.center.copy(this.translationRotationAnchor.position)
/** Apply rotation snapping if shift key is pressed */
let quaternion = this.translationRotationAnchor.quaternion
if (this.shiftKeyPressed) {
quaternion = this.snapQuaternionToGrid(quaternion)
/** Update the anchor's quaternion to keep visual controls in sync */
this.translationRotationAnchor.quaternion.copy(quaternion)
}
this.obb.rotation.copy(
new Matrix3().setFromMatrix4(
new Matrix4().makeRotationFromQuaternion(
this.translationRotationAnchor.quaternion
)
)
new Matrix3().setFromMatrix4(new Matrix4().makeRotationFromQuaternion(quaternion))
)
this.obb.halfSize.copy(this.scaleAnchor.scale)
@@ -920,4 +1041,102 @@ export class SectionTool extends Extension {
): box is OBB {
return box instanceof OBB
}
/**
* Snaps a quaternion to the nearest grid based on rotationSnapAngle.
* This is useful for rotation snapping.
* @param q The quaternion to snap.
* @returns The snapped quaternion.
*/
protected snapQuaternionToGrid(q: Quaternion): Quaternion {
/** Convert quaternion to Euler angles using pooled object */
_tempEuler.setFromQuaternion(q)
/** Snap each axis to the configured angle increments */
if (this.rotationSnapAngle !== null) {
_tempEuler.x =
Math.round(_tempEuler.x / this.rotationSnapAngle) * this.rotationSnapAngle
_tempEuler.y =
Math.round(_tempEuler.y / this.rotationSnapAngle) * this.rotationSnapAngle
_tempEuler.z =
Math.round(_tempEuler.z / this.rotationSnapAngle) * this.rotationSnapAngle
}
/** Convert back to quaternion using pooled object */
_tempQuaternion.setFromEuler(_tempEuler)
return _tempQuaternion
}
/**
* Undoes the last section box change
*/
protected undoSectionBox() {
if (this.currentHistoryIndex > 0) {
/** Move cursor back */
this.currentHistoryIndex--
/** Get the state at current cursor position */
const previousState = this.sectionBoxHistory[this.currentHistoryIndex]
if (previousState) {
/** Apply the previous state */
this.applyObbState(previousState)
/** Update visual state */
this.updatePlanes()
this.updateVisual()
this.updateFaceControls(this.draggingFace)
/** Update section outlines */
const sectionOutlines = this.viewer.getExtension(SectionOutlines)
if (sectionOutlines && sectionOutlines.enabled) {
sectionOutlines.requestUpdate(true)
}
this.viewer.requestRender()
}
}
}
/**
* Redoes the last undone section box change
*/
protected redoSectionBox() {
if (this.currentHistoryIndex < this.sectionBoxHistory.length - 1) {
/** Move cursor forward */
this.currentHistoryIndex++
/** Get the state at current cursor position */
const nextState = this.sectionBoxHistory[this.currentHistoryIndex]
if (nextState) {
/** Apply the next state */
this.applyObbState(nextState)
/** Update visual state */
this.updatePlanes()
this.updateVisual()
this.updateFaceControls(this.draggingFace)
/** Update section outlines */
const sectionOutlines = this.viewer.getExtension(SectionOutlines)
if (sectionOutlines && sectionOutlines.enabled) {
sectionOutlines.requestUpdate(true)
}
this.viewer.requestRender()
}
}
}
/**
* Cleanup method to remove event listeners and prevent memory leaks
*/
public dispose() {
if (this.keydownHandler) {
document.removeEventListener('keydown', this.keydownHandler)
}
if (this.keyupHandler) {
document.removeEventListener('keyup', this.keyupHandler)
}
}
}
@@ -1148,6 +1148,12 @@ Generate the environment variables for Speckle server and Speckle objects deploy
- name: FF_NEXT_GEN_FILE_IMPORTER_ENABLED
value: {{ .Values.featureFlags.nextGenFileImporterEnabled | quote }}
{{- end }}
{{- if .Values.featureFlags.rhinoFileImporterEnabled }}
- name: FF_RHINO_FILE_IMPORTER_ENABLED
value: {{ .Values.featureFlags.rhinoFileImporterEnabled | quote }}
{{- end }}
{{- if .Values.featureFlags.backgroundJobsEnabled }}
- name: FILEIMPORT_QUEUE_POSTGRES_URL
valueFrom:
@@ -129,6 +129,11 @@
"type": "boolean",
"description": "Enables the ability to run background jobs (such as the IFC importer) in Speckle",
"default": false
},
"rhinoFileImporterEnabled": {
"type": "boolean",
"description": "Enables the dedicated Rhino based file importer. This is not part of the deployment.",
"default": false
}
}
},
+2
View File
@@ -73,6 +73,8 @@ featureFlags:
legacyIfcImporterEnabled: false
## @param featureFlags.backgroundJobsEnabled Enables the ability to run background jobs (such as the IFC importer) in Speckle
backgroundJobsEnabled: false
## @param featureFlags.rhinoFileImporterEnabled Enables the dedicated Rhino based file importer. This is not part of the deployment.
rhinoFileImporterEnabled: false
analytics:
## @param analytics.enabled Enable or disable analytics